| // 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) |
| ) |
| |
| func initFlags() { |
| flags = flag.NewFlagSet("", flag.ExitOnError) |
| flags.Usage = func() { |
| fmt.Fprintf(stderr(), usage, os.Args[0], os.Args[0]) |
| } |
| flags.Var(verbose, "v", "report commands") |
| flags.BoolVar(noRun, "n", false, "print but do not run commands") |
| } |
| |
| const globalFlags = "[-n] [-v]" |
| |
| const usage = `Usage: %s <command> ` + globalFlags + ` |
| Type "%s help" for more information. |
| ` |
| |
| const help = `Usage: %s <command> ` + globalFlags + ` |
| |
| The git-codereview command is a wrapper for the git command that provides a |
| simple interface to the "single-commit feature branch" development model. |
| |
| See the docs for details: https://godoc.org/golang.org/x/review/git-codereview |
| |
| The -v flag prints all commands that make changes. |
| The -n flag prints all commands that would be run, but does not run them. |
| |
| Available commands: |
| |
| change [name] |
| Create a change commit, or amend an existing change commit, |
| with the staged changes. If a branch name is provided, check |
| out that branch (creating it if it does not exist). |
| Does not amend the existing commit when switching branches. |
| If -q is specified, skip the editing of an extant pending |
| change's commit message. |
| If -a is specified, automatically add any unstaged changes in |
| tracked files during commit. |
| If -m is specified and a message given, a commit is created |
| and the editor prompt is skipped. |
| |
| change NNNN[/PP] |
| Checkout the commit corresponding to CL number NNNN and |
| patch set PP from Gerrit. |
| If the patch set is omitted, use the current patch set. |
| |
| gofmt [-l] |
| Run gofmt on all tracked files in the staging area and the |
| working tree. |
| If -l is specified, list files that need formatting. |
| Otherwise, reformat files in place. |
| |
| help |
| Show this help text. |
| |
| hooks |
| Install Git commit hooks for Gerrit and gofmt. |
| Every other operation except help also does this, |
| if they are not already installed. |
| |
| mail [-f] [-r reviewer,...] [-cc mail,...] [-trybot] [-hashtag tag,...] [commit] |
| Upload change commit to the code review server and send mail |
| requesting a code review. |
| If there are multiple commits on this branch, upload commits |
| up to and including the named commit. |
| If -f is specified, upload even if there are staged changes. |
| The -r and -cc flags identify the email addresses of people to |
| do the code review and to be CC'ed about the code review. |
| Multiple addresses are given as a comma-separated list. |
| If -trybot is specified, the trybots are run on the changes, |
| if permitted. |
| The -hashtag flag applies hashtags to the code review. |
| |
| mail -diff |
| Show the changes but do not send mail or upload. |
| |
| pending [-c] [-l] [-s] |
| Show the status of all pending changes and staged, unstaged, |
| and untracked files in the local repository. |
| If -c is specified, show only changes on the current branch. |
| If -l is specified, only use locally available information. |
| If -s is specified, show short output. |
| |
| rebase-work |
| Run git rebase in interactive mode over pending changes |
| (shorthand for "git rebase -i $(git codereview branchpoint)"). |
| This rebase does not incorporate any new changes from the origin |
| branch, in contrast with a normal "git rebase -i". |
| |
| submit [-i | commit...] |
| Push the pending change to the Gerrit server and tell Gerrit to |
| submit it to the master branch. |
| |
| sync |
| Fetch changes from the remote repository and merge them into |
| the current branch, rebasing the change commit on top of them. |
| |
| |
| ` |
| |
| func main() { |
| initFlags() |
| |
| if len(os.Args) < 2 { |
| flags.Usage() |
| if dieTrap != nil { |
| dieTrap() |
| } |
| os.Exit(2) |
| } |
| command, args := os.Args[1], os.Args[2:] |
| |
| var cmd func([]string) |
| switch command { |
| case "help": |
| fmt.Fprintf(stdout(), help, os.Args[0]) |
| 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 "submit": |
| cmd = cmdSubmit |
| case "sync": |
| cmd = cmdSync |
| case "test-loadAuth": // for testing only. |
| cmd = func([]string) { loadAuth() } |
| default: |
| flags.Usage() |
| return // avoid installing hooks. |
| } |
| |
| // 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", os.Args[0], command, globalFlags) |
| os.Exit(2) |
| } |
| } |
| |
| 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() |
| 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 |
| } |
| 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...), " ") |
| } |
| |
| var dieTrap func() |
| |
| func dief(format string, args ...interface{}) { |
| printf(format, args...) |
| die() |
| } |
| |
| func die() { |
| if dieTrap != nil { |
| dieTrap() |
| } |
| os.Exit(1) |
| } |
| |
| 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", os.Args[0], 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 |
| } |