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 = let res = exec("git", repoDir, args, env, {poUsePath}, outputHandler) return res.exitCode == 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] [...] Options: -h --help Print this usage information and exit. -v --verbose Enable verbose logging. -V --version Print version -r --remote Pull from (defaults to "origin") -b --branch Pull the branch (defaults to "master") """ let args = docopt(doc, version = "git_pull_all 0.1.0") let rootDirs: seq[string] = if args[""]: args[""].mapIt(it) else: @["."] let remote = if args["--remote"]: $args[""] else: "origin" let branch = if args["--branch"]: $args[""] 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)) # Foreach repo: for repoDir in repos: let repoName = repoDir.extractFilename 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) # Is the repo clean? No -> fail to pull this repo cmdOutput.clear() if not git(repoDir, ["status"]) or cmdOutput.outBuf.join("\n").find("working tree clean") < 0: failRepo("working directory is not clean") continue # 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 if cmdOutput.outBuf.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) # 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() except: stderr.writeLine "git_pull_all: " & getCurrentExceptionMsg()