blob: 1e79114c23709aaedac0cc3a29186b4c062c9681 [file] [log] [blame]
// 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
}