| // Copyright 2014 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // TODO(rsc): Document multi-change branch behavior. |
| |
| // Command git-codereview provides a simple command-line user interface for |
| // working with git repositories and the Gerrit code review system. |
| // See "git-codereview help" for details. |
| package main // import "golang.org/x/review/git-codereview" |
| |
| import ( |
| "bytes" |
| "flag" |
| "fmt" |
| "io" |
| "os" |
| "os/exec" |
| "strconv" |
| "strings" |
| "time" |
| ) |
| |
| var ( |
| flags *flag.FlagSet |
| verbose = new(count) // installed as -v below |
| noRun = new(bool) |
| ) |
| |
| const progName = "git-codereview" |
| |
| func initFlags() { |
| flags = flag.NewFlagSet("", flag.ExitOnError) |
| flags.Usage = func() { |
| fmt.Fprintf(stderr(), usage, progName, progName) |
| exit(2) |
| } |
| flags.SetOutput(stderr()) |
| flags.BoolVar(noRun, "n", false, "print but do not run commands") |
| flags.Var(verbose, "v", "report commands") |
| } |
| |
| const globalFlags = "[-n] [-v]" |
| |
| const usage = `Usage: %s <command> ` + globalFlags + ` |
| |
| Use "%s help" for a list of commands. |
| ` |
| |
| const help = `Usage: %s <command> ` + globalFlags + ` |
| |
| Git-codereview is a git helper command for managing pending commits |
| against an upstream server, typically a Gerrit server. |
| |
| The -n flag prints commands that would make changes but does not run them. |
| The -v flag prints those commands as they run. |
| |
| Available commands: |
| |
| branchpoint |
| change [name] |
| change NNNN[/PP] |
| gofmt [-l] |
| help |
| hooks |
| mail [-r reviewer,...] [-cc mail,...] [options] [commit] |
| pending [-c] [-l] [-s] |
| rebase-work |
| reword [commit...] |
| submit [-i | commit...] |
| sync |
| sync-branch [-continue] |
| |
| See https://pkg.go.dev/golang.org/x/review/git-codereview |
| for the full details of each command. |
| ` |
| |
| func main() { |
| initFlags() |
| |
| if len(os.Args) < 2 { |
| flags.Usage() |
| exit(2) |
| } |
| command, args := os.Args[1], os.Args[2:] |
| |
| // NOTE: Keep this switch in sync with the list of commands above. |
| var cmd func([]string) |
| switch command { |
| default: |
| flags.Usage() |
| exit(2) // avoid installing hooks. |
| case "help": |
| fmt.Fprintf(stdout(), help, progName) |
| return // avoid installing hooks. |
| case "hooks": // in case hooks weren't installed. |
| installHook(args) |
| return // avoid invoking installHook twice. |
| |
| case "branchpoint": |
| cmd = cmdBranchpoint |
| case "change": |
| cmd = cmdChange |
| case "gofmt": |
| cmd = cmdGofmt |
| case "hook-invoke": |
| cmd = cmdHookInvoke |
| case "mail", "m": |
| cmd = cmdMail |
| case "pending": |
| cmd = cmdPending |
| case "rebase-work": |
| cmd = cmdRebaseWork |
| case "reword": |
| cmd = cmdReword |
| case "submit": |
| cmd = cmdSubmit |
| case "sync": |
| cmd = cmdSync |
| case "sync-branch": |
| cmd = cmdSyncBranch |
| case "test-loadAuth": // for testing only. |
| cmd = func([]string) { loadAuth() } |
| } |
| |
| // Install hooks automatically, but only if this is a Gerrit repo. |
| if haveGerrit() { |
| // Don't pass installHook args directly, |
| // since args might contain args meant for other commands. |
| // Filter down to just global flags. |
| var hookArgs []string |
| for _, arg := range args { |
| switch arg { |
| case "-n", "-v": |
| hookArgs = append(hookArgs, arg) |
| } |
| } |
| installHook(hookArgs) |
| } |
| |
| cmd(args) |
| } |
| |
| func expectZeroArgs(args []string, command string) { |
| flags.Parse(args) |
| if len(flags.Args()) > 0 { |
| fmt.Fprintf(stderr(), "Usage: %s %s %s\n", progName, command, globalFlags) |
| exit(2) |
| } |
| } |
| |
| func setEnglishLocale(cmd *exec.Cmd) { |
| // Override the existing locale to prevent non-English locales from |
| // interfering with string parsing. See golang.org/issue/33895. |
| if cmd.Env == nil { |
| cmd.Env = os.Environ() |
| } |
| cmd.Env = append(cmd.Env, "LC_ALL=C") |
| } |
| |
| func run(command string, args ...string) { |
| if err := runErr(command, args...); err != nil { |
| if *verbose == 0 { |
| // If we're not in verbose mode, print the command |
| // before dying to give context to the failure. |
| fmt.Fprintf(stderr(), "(running: %s)\n", commandString(command, args)) |
| } |
| dief("%v", err) |
| } |
| } |
| |
| func runErr(command string, args ...string) error { |
| return runDirErr(".", command, args...) |
| } |
| |
| var runLogTrap []string |
| |
| func runDirErr(dir, command string, args ...string) error { |
| if *noRun || *verbose == 1 { |
| fmt.Fprintln(stderr(), commandString(command, args)) |
| } else if *verbose > 1 { |
| start := time.Now() |
| defer func() { |
| fmt.Fprintf(stderr(), "%s # %.3fs\n", commandString(command, args), time.Since(start).Seconds()) |
| }() |
| } |
| if *noRun { |
| return nil |
| } |
| if runLogTrap != nil { |
| runLogTrap = append(runLogTrap, strings.TrimSpace(command+" "+strings.Join(args, " "))) |
| } |
| cmd := exec.Command(command, args...) |
| cmd.Stdin = os.Stdin |
| cmd.Stdout = stdout() |
| cmd.Stderr = stderr() |
| if dir != "." { |
| cmd.Dir = dir |
| } |
| setEnglishLocale(cmd) |
| return cmd.Run() |
| } |
| |
| // cmdOutput runs the command line, returning its output. |
| // If the command cannot be run or does not exit successfully, |
| // cmdOutput dies. |
| // |
| // NOTE: cmdOutput must be used only to run commands that read state, |
| // not for commands that make changes. Commands that make changes |
| // should be run using runDirErr so that the -v and -n flags apply to them. |
| func cmdOutput(command string, args ...string) string { |
| return cmdOutputDir(".", command, args...) |
| } |
| |
| // cmdOutputDir runs the command line in dir, returning its output. |
| // If the command cannot be run or does not exit successfully, |
| // cmdOutput dies. |
| // |
| // NOTE: cmdOutput must be used only to run commands that read state, |
| // not for commands that make changes. Commands that make changes |
| // should be run using runDirErr so that the -v and -n flags apply to them. |
| func cmdOutputDir(dir, command string, args ...string) string { |
| s, err := cmdOutputDirErr(dir, command, args...) |
| if err != nil { |
| fmt.Fprintf(stderr(), "%v\n%s\n", commandString(command, args), s) |
| dief("%v", err) |
| } |
| return s |
| } |
| |
| // cmdOutputErr runs the command line in dir, returning its output |
| // and any error results. |
| // |
| // NOTE: cmdOutputErr must be used only to run commands that read state, |
| // not for commands that make changes. Commands that make changes |
| // should be run using runDirErr so that the -v and -n flags apply to them. |
| func cmdOutputErr(command string, args ...string) (string, error) { |
| return cmdOutputDirErr(".", command, args...) |
| } |
| |
| // cmdOutputDirErr runs the command line in dir, returning its output |
| // and any error results. |
| // |
| // NOTE: cmdOutputDirErr must be used only to run commands that read state, |
| // not for commands that make changes. Commands that make changes |
| // should be run using runDirErr so that the -v and -n flags apply to them. |
| func cmdOutputDirErr(dir, command string, args ...string) (string, error) { |
| // NOTE: We only show these non-state-modifying commands with -v -v. |
| // Otherwise things like 'git sync -v' show all our internal "find out about |
| // the git repo" commands, which is confusing if you are just trying to find |
| // out what git sync means. |
| if *verbose > 1 { |
| start := time.Now() |
| defer func() { |
| fmt.Fprintf(stderr(), "%s # %.3fs\n", commandString(command, args), time.Since(start).Seconds()) |
| }() |
| } |
| cmd := exec.Command(command, args...) |
| if dir != "." { |
| cmd.Dir = dir |
| } |
| setEnglishLocale(cmd) |
| b, err := cmd.CombinedOutput() |
| return string(b), err |
| } |
| |
| // trim is shorthand for strings.TrimSpace. |
| func trim(text string) string { |
| return strings.TrimSpace(text) |
| } |
| |
| // trimErr applies strings.TrimSpace to the result of cmdOutput(Dir)Err, |
| // passing the error along unmodified. |
| func trimErr(text string, err error) (string, error) { |
| return strings.TrimSpace(text), err |
| } |
| |
| // lines returns the lines in text. |
| func lines(text string) []string { |
| out := strings.Split(text, "\n") |
| // Split will include a "" after the last line. Remove it. |
| if n := len(out) - 1; n >= 0 && out[n] == "" { |
| out = out[:n] |
| } |
| return out |
| } |
| |
| // nonBlankLines returns the non-blank lines in text. |
| func nonBlankLines(text string) []string { |
| var out []string |
| for _, s := range lines(text) { |
| if strings.TrimSpace(s) != "" { |
| out = append(out, s) |
| } |
| } |
| return out |
| } |
| |
| func commandString(command string, args []string) string { |
| return strings.Join(append([]string{command}, args...), " ") |
| } |
| |
| func dief(format string, args ...interface{}) { |
| printf(format, args...) |
| exit(1) |
| } |
| |
| var exitTrap func() |
| |
| func exit(code int) { |
| if exitTrap != nil { |
| exitTrap() |
| } |
| os.Exit(code) |
| } |
| |
| func verbosef(format string, args ...interface{}) { |
| if *verbose > 0 { |
| printf(format, args...) |
| } |
| } |
| |
| var stdoutTrap, stderrTrap *bytes.Buffer |
| |
| func stdout() io.Writer { |
| if stdoutTrap != nil { |
| return stdoutTrap |
| } |
| return os.Stdout |
| } |
| |
| func stderr() io.Writer { |
| if stderrTrap != nil { |
| return stderrTrap |
| } |
| return os.Stderr |
| } |
| |
| func printf(format string, args ...interface{}) { |
| fmt.Fprintf(stderr(), "%s: %s\n", progName, fmt.Sprintf(format, args...)) |
| } |
| |
| // count is a flag.Value that is like a flag.Bool and a flag.Int. |
| // If used as -name, it increments the count, but -name=x sets the count. |
| // Used for verbose flag -v. |
| type count int |
| |
| func (c *count) String() string { |
| return fmt.Sprint(int(*c)) |
| } |
| |
| func (c *count) Set(s string) error { |
| switch s { |
| case "true": |
| *c++ |
| case "false": |
| *c = 0 |
| default: |
| n, err := strconv.Atoi(s) |
| if err != nil { |
| return fmt.Errorf("invalid count %q", s) |
| } |
| *c = count(n) |
| } |
| return nil |
| } |
| |
| func (c *count) IsBoolFlag() bool { |
| return true |
| } |