195 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Nim
		
	
	
	
	
	
			
		
		
	
	
			195 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Nim
		
	
	
	
	
	
| import cliutils, docopt, os, ospaths, osproc, sequtils, strutils, terminal
 | |
| 
 | |
| type
 | |
|   OutputBuffer = ref object
 | |
|     outBuf*, errBuf*: seq[string]
 | |
| 
 | |
| let env = loadEnv()
 | |
| var outputHandler: HandleProcMsgCB = nil
 | |
| var verbose = false
 | |
| var successRepos: seq[string] = @[]
 | |
| var failRepos: seq[tuple[name, err: string]] = @[]
 | |
| 
 | |
| proc makeBufferLogger(b: OutputBuffer): HandleProcMsgCB =
 | |
|   result = proc(outMsg, errMsg: TaintedString, cmd: string): void {.closure.} =
 | |
|     let prefix = if cmd != nil: cmd & ": " else: ""
 | |
|     if outMsg != nil: b.outBuf.add(prefix & outMsg)
 | |
|     if errMsg != nil: b.errBuf.add(prefix & errMsg)
 | |
| 
 | |
| proc clear(buf: OutputBuffer): void =
 | |
|   buf.outBuf = @[]
 | |
|   buf.errBuf = @[]
 | |
| 
 | |
| proc `$`(buf: OutputBuffer): string =
 | |
|   buf.outBuf.join("\n") & "\n" & buf.errBuf.join("\n")
 | |
| 
 | |
| proc git(repoDir: string, args: openArray[string]): bool =
 | |
|   if verbose:
 | |
|     stdout.setForegroundColor(fgBlue, false)
 | |
|     stdout.writeLine("> git " & toSeq(args.items).mapIt("'" & it & "'").join(" "))
 | |
|     stdout.resetAttributes()
 | |
| 
 | |
|   return exec("git", repoDir, args, env, {poUsePath}, outputHandler) == 0
 | |
| 
 | |
| proc isGitRepo(dir: string): bool =
 | |
|   if not existsDir(dir): return false
 | |
|   return git(dir, ["rev-parse", "--git-dir"])
 | |
| 
 | |
| proc findGitRepos(rootDir: string): seq[string] =
 | |
|   return toSeq(rootDir.walkDir())
 | |
|     .filterIt(it.kind == pcDir and isGitRepo(it.path))
 | |
|     .mapIt(it.path.expandFilename)
 | |
| 
 | |
| proc writeColoredOutput(b: OutputBuffer): void =
 | |
|   stdout.writeLine(b.outBuf.join("\n"))
 | |
|   stdout.setForegroundColor(fgRed, false)
 | |
|   stdout.writeLine(b.errBuf.join("\n"))
 | |
|   stdout.resetAttributes()
 | |
| 
 | |
| proc writeErrMsg(msg: string): void =
 | |
|   stdout.setForegroundColor(fgRed, false)
 | |
|   stdout.writeLine(msg)
 | |
|   stdout.resetAttributes()
 | |
| 
 | |
| when isMainModule:
 | |
|   try:
 | |
|     let doc = """
 | |
| Usage:
 | |
|   git_pull_all [options] [<root>...]
 | |
| 
 | |
| Options:
 | |
| 
 | |
|   -h --help             Print this usage information and exit.
 | |
|   -v --verbose          Enable verbose logging.
 | |
|   -V --version          Print version
 | |
|   -r --remote <remote>  Pull from <remote> (defaults to "origin")
 | |
|   -b --branch <branch>  Pull the <branch> branch (defaults to "master")
 | |
|   -o --log-out <outlog> Log command output to <outfile>
 | |
|   -e --log-err <errlog> Log error output to <errfile>
 | |
| """
 | |
| 
 | |
|     let args = docopt(doc, version = "git_pull_all 0.2.1")
 | |
| 
 | |
|     let rootDirs: seq[string] =
 | |
|       if args["<root>"]: args["<root>"].mapIt(it)
 | |
|       else: @["."]
 | |
| 
 | |
|     let remote =
 | |
|       if args["--remote"]: $args["<remote>"]
 | |
|       else: "origin"
 | |
| 
 | |
|     let branch =
 | |
|       if args["--branch"]: $args["<branch>"]
 | |
|       else: "master"
 | |
| 
 | |
|     # Get a list of repos -> absolute directory paths.
 | |
|     #let repos = rootDirs.map(proc (dir: string): openArray[string] =
 | |
|     let repos = rootDirs.mapIt(findGitRepos(it)).concat()
 | |
| 
 | |
|     let cmdOutput = OutputBuffer(outBuf: @[], errBuf: @[])
 | |
|     outputHandler = makeBufferLogger(cmdOutput)
 | |
| 
 | |
|     if args["--verbose"]:
 | |
|       verbose = true
 | |
|       outputHandler = combineProcMsgHandlers(outputHandler,
 | |
|         makeProcMsgHandler(stdout, stderr))
 | |
| 
 | |
|     var outLog, errLog: File = nil
 | |
|     if args["--log-out"]: outLog = open($args["<outlog>"], fmRead)
 | |
|     if args["--log-err"]: errLog = open($args["<errlog>"], fmRead)
 | |
| 
 | |
|     if outLog != nil or errLog != nil:
 | |
|       outputHandler = combineProcMsgHandlers(outputHandler,
 | |
|         makeProcMsgHandler(outLog, errLog))
 | |
| 
 | |
|     # Foreach repo:
 | |
|     for repoDir in repos:
 | |
|       let repoName = repoDir.extractFilename
 | |
| 
 | |
|       if verbose: stdout.writeLine("")
 | |
|       stdout.setForegroundColor(fgCyan, true)
 | |
|       stdout.write("Pulling ")
 | |
|       stdout.setForegroundColor(fgYellow, false)
 | |
|       stdout.write(repoName & "... ")
 | |
|       stdout.resetAttributes()
 | |
|       if verbose: stdout.writeLine("")
 | |
| 
 | |
|       let failRepo = proc(reason: string): void =
 | |
|         failRepos.add((repoName, reason))
 | |
|         writeErrMsg(reason)
 | |
| 
 | |
|       var pullOutput: seq[string]
 | |
| 
 | |
|       # Is the a bare repo clean?
 | |
|       cmdOutput.clear()
 | |
|       if not git(repoDir, ["status"]):
 | |
| 
 | |
|         # pull --ff-only
 | |
|         if not git(repoDir, ["pull", "--ff-only", remote, branch]):
 | |
|           failRepo("unable to ffwd branch")
 | |
|           continue
 | |
| 
 | |
|         pullOutput = cmdOutput.outBuf
 | |
| 
 | |
|       # Not bare
 | |
|       else:
 | |
|         # Not clean? Try to stash the changes.
 | |
|         var stashed = false
 | |
|         if cmdOutput.outBuf.join("\n").find("working tree clean") < 0 and
 | |
|            cmdOutput.outBuf.join("\n").find("working directory clean") < 0:
 | |
|           cmdOutput.clear()
 | |
|           if not git(repoDir, ["stash", "save"]):
 | |
|             failRepo("error trying to stash uncommitted changes")
 | |
|             continue
 | |
|           else: stashed = true
 | |
| 
 | |
|         # Are we on the correct branch?
 | |
|         cmdOutput.clear()
 | |
|         if not git(repoDir, ["branch"]):
 | |
|           failRepo("could not get current branch")
 | |
|           continue
 | |
| 
 | |
|         let branches = cmdOutput.outBuf.filterIt(it.find("* ") > 0)
 | |
|         let currentBranch =
 | |
|           if branches.len == 1: branches[0][7..^1]
 | |
|           else: nil
 | |
| 
 | |
|         # not on correct branch, switch branch
 | |
|         if currentBranch != branch:
 | |
|           if not git(repoDir, ["checkout", branch]):
 | |
|             failRepo("could not check out " & branch & " branch")
 | |
|             continue
 | |
| 
 | |
|         # pull --ff-only
 | |
|         if not git(repoDir, ["pull", "--ff-only", remote, branch]):
 | |
|           failRepo("unable to ffwd branch")
 | |
|           continue
 | |
| 
 | |
|         pullOutput = cmdOutput.outBuf
 | |
| 
 | |
|         # restore original branch
 | |
|         if currentBranch != branch:
 | |
|           if not git(repoDir, ["checkout", currentBranch]):
 | |
|             stdout.setForegroundColor(fgWhite, true)
 | |
|             stdout.writeLine("WARNING: unable to checkout original branch (" & currentBranch & ")")
 | |
|             stdout.resetAttributes()
 | |
| 
 | |
|         # restore stashed changes
 | |
|         if stashed:
 | |
|           if not git(repoDir, ["stash", "pop"]):
 | |
|             stdout.setForegroundColor(fgWhite, true)
 | |
|             stdout.writeLine("WARNING: unable to pop stashed changes")
 | |
|             stdout.resetAttributes()
 | |
| 
 | |
|       if pullOutput.anyIt(it.find("Already up-to-date") > 0):
 | |
|         stdout.setForegroundColor(fgBlack, true)
 | |
|         stdout.writeLine("already up-to-date")
 | |
|       else:
 | |
|         stdout.setForegroundColor(fgGreen, true)
 | |
|         stdout.writeLine("UPDATED")
 | |
|       stdout.resetAttributes()
 | |
|       successRepos.add(repoName)
 | |
| 
 | |
|   except:
 | |
|     stderr.writeLine "git_pull_all: " & getCurrentExceptionMsg()
 |