1// Copyright 2014 The Go Authors.  All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// TODO(rsc): Document multi-change branch behavior.
6
7// Command git-codereview provides a simple command-line user interface for
8// working with git repositories and the Gerrit code review system.
9// See "git-codereview help" for details.
10package main // import "golang.org/x/review/git-codereview"
11
12import (
13	"bytes"
14	"flag"
15	"fmt"
16	"io"
17	"os"
18	"os/exec"
19	"strconv"
20	"strings"
21	"time"
22)
23
24var (
25	flags   *flag.FlagSet
26	verbose = new(count) // installed as -v below
27	noRun   = new(bool)
28)
29
30const progName = "git-codereview"
31
32func initFlags() {
33	flags = flag.NewFlagSet("", flag.ExitOnError)
34	flags.Usage = func() {
35		fmt.Fprintf(stderr(), usage, progName, progName)
36		exit(2)
37	}
38	flags.SetOutput(stderr())
39	flags.BoolVar(noRun, "n", false, "print but do not run commands")
40	flags.Var(verbose, "v", "report commands")
41}
42
43const globalFlags = "[-n] [-v]"
44
45const usage = `Usage: %s <command> ` + globalFlags + `
46
47Use "%s help" for a list of commands.
48`
49
50const help = `Usage: %s <command> ` + globalFlags + `
51
52Git-codereview is a git helper command for managing pending commits
53against an upstream server, typically a Gerrit server.
54
55The -n flag prints commands that would make changes but does not run them.
56The -v flag prints those commands as they run.
57
58Available commands:
59
60	branchpoint
61	change [name]
62	change NNNN[/PP]
63	gofmt [-l]
64	help
65	hooks
66	mail [-r reviewer,...] [-cc mail,...] [options] [commit]
67	pending [-c] [-l] [-s]
68	rebase-work
69	reword [commit...]
70	submit [-i | commit...]
71	sync
72	sync-branch [-continue]
73
74See https://pkg.go.dev/golang.org/x/review/git-codereview
75for the full details of each command.
76`
77
78func main() {
79	initFlags()
80
81	if len(os.Args) < 2 {
82		flags.Usage()
83		exit(2)
84	}
85	command, args := os.Args[1], os.Args[2:]
86
87	// NOTE: Keep this switch in sync with the list of commands above.
88	var cmd func([]string)
89	switch command {
90	default:
91		flags.Usage()
92		exit(2) // avoid installing hooks.
93	case "help":
94		fmt.Fprintf(stdout(), help, progName)
95		return // avoid installing hooks.
96	case "hooks": // in case hooks weren't installed.
97		installHook(args)
98		return // avoid invoking installHook twice.
99
100	case "branchpoint":
101		cmd = cmdBranchpoint
102	case "change":
103		cmd = cmdChange
104	case "gofmt":
105		cmd = cmdGofmt
106	case "hook-invoke":
107		cmd = cmdHookInvoke
108	case "mail", "m":
109		cmd = cmdMail
110	case "pending":
111		cmd = cmdPending
112	case "rebase-work":
113		cmd = cmdRebaseWork
114	case "reword":
115		cmd = cmdReword
116	case "submit":
117		cmd = cmdSubmit
118	case "sync":
119		cmd = cmdSync
120	case "sync-branch":
121		cmd = cmdSyncBranch
122	case "test-loadAuth": // for testing only.
123		cmd = func([]string) { loadAuth() }
124	}
125
126	// Install hooks automatically, but only if this is a Gerrit repo.
127	if haveGerrit() {
128		// Don't pass installHook args directly,
129		// since args might contain args meant for other commands.
130		// Filter down to just global flags.
131		var hookArgs []string
132		for _, arg := range args {
133			switch arg {
134			case "-n", "-v":
135				hookArgs = append(hookArgs, arg)
136			}
137		}
138		installHook(hookArgs)
139	}
140
141	cmd(args)
142}
143
144func expectZeroArgs(args []string, command string) {
145	flags.Parse(args)
146	if len(flags.Args()) > 0 {
147		fmt.Fprintf(stderr(), "Usage: %s %s %s\n", progName, command, globalFlags)
148		exit(2)
149	}
150}
151
152func setEnglishLocale(cmd *exec.Cmd) {
153	// Override the existing locale to prevent non-English locales from
154	// interfering with string parsing. See golang.org/issue/33895.
155	if cmd.Env == nil {
156		cmd.Env = os.Environ()
157	}
158	cmd.Env = append(cmd.Env, "LC_ALL=C")
159}
160
161func run(command string, args ...string) {
162	if err := runErr(command, args...); err != nil {
163		if *verbose == 0 {
164			// If we're not in verbose mode, print the command
165			// before dying to give context to the failure.
166			fmt.Fprintf(stderr(), "(running: %s)\n", commandString(command, args))
167		}
168		dief("%v", err)
169	}
170}
171
172func runErr(command string, args ...string) error {
173	return runDirErr(".", command, args...)
174}
175
176var runLogTrap []string
177
178func runDirErr(dir, command string, args ...string) error {
179	if *noRun || *verbose == 1 {
180		fmt.Fprintln(stderr(), commandString(command, args))
181	} else if *verbose > 1 {
182		start := time.Now()
183		defer func() {
184			fmt.Fprintf(stderr(), "%s # %.3fs\n", commandString(command, args), time.Since(start).Seconds())
185		}()
186	}
187	if *noRun {
188		return nil
189	}
190	if runLogTrap != nil {
191		runLogTrap = append(runLogTrap, strings.TrimSpace(command+" "+strings.Join(args, " ")))
192	}
193	cmd := exec.Command(command, args...)
194	cmd.Stdin = os.Stdin
195	cmd.Stdout = stdout()
196	cmd.Stderr = stderr()
197	if dir != "." {
198		cmd.Dir = dir
199	}
200	setEnglishLocale(cmd)
201	return cmd.Run()
202}
203
204// cmdOutput runs the command line, returning its output.
205// If the command cannot be run or does not exit successfully,
206// cmdOutput dies.
207//
208// NOTE: cmdOutput must be used only to run commands that read state,
209// not for commands that make changes. Commands that make changes
210// should be run using runDirErr so that the -v and -n flags apply to them.
211func cmdOutput(command string, args ...string) string {
212	return cmdOutputDir(".", command, args...)
213}
214
215// cmdOutputDir runs the command line in dir, returning its output.
216// If the command cannot be run or does not exit successfully,
217// cmdOutput dies.
218//
219// NOTE: cmdOutput must be used only to run commands that read state,
220// not for commands that make changes. Commands that make changes
221// should be run using runDirErr so that the -v and -n flags apply to them.
222func cmdOutputDir(dir, command string, args ...string) string {
223	s, err := cmdOutputDirErr(dir, command, args...)
224	if err != nil {
225		fmt.Fprintf(stderr(), "%v\n%s\n", commandString(command, args), s)
226		dief("%v", err)
227	}
228	return s
229}
230
231// cmdOutputErr runs the command line in dir, returning its output
232// and any error results.
233//
234// NOTE: cmdOutputErr must be used only to run commands that read state,
235// not for commands that make changes. Commands that make changes
236// should be run using runDirErr so that the -v and -n flags apply to them.
237func cmdOutputErr(command string, args ...string) (string, error) {
238	return cmdOutputDirErr(".", command, args...)
239}
240
241// cmdOutputDirErr runs the command line in dir, returning its output
242// and any error results.
243//
244// NOTE: cmdOutputDirErr must be used only to run commands that read state,
245// not for commands that make changes. Commands that make changes
246// should be run using runDirErr so that the -v and -n flags apply to them.
247func cmdOutputDirErr(dir, command string, args ...string) (string, error) {
248	// NOTE: We only show these non-state-modifying commands with -v -v.
249	// Otherwise things like 'git sync -v' show all our internal "find out about
250	// the git repo" commands, which is confusing if you are just trying to find
251	// out what git sync means.
252	if *verbose > 1 {
253		start := time.Now()
254		defer func() {
255			fmt.Fprintf(stderr(), "%s # %.3fs\n", commandString(command, args), time.Since(start).Seconds())
256		}()
257	}
258	cmd := exec.Command(command, args...)
259	if dir != "." {
260		cmd.Dir = dir
261	}
262	setEnglishLocale(cmd)
263	b, err := cmd.CombinedOutput()
264	return string(b), err
265}
266
267// trim is shorthand for strings.TrimSpace.
268func trim(text string) string {
269	return strings.TrimSpace(text)
270}
271
272// trimErr applies strings.TrimSpace to the result of cmdOutput(Dir)Err,
273// passing the error along unmodified.
274func trimErr(text string, err error) (string, error) {
275	return strings.TrimSpace(text), err
276}
277
278// lines returns the lines in text.
279func lines(text string) []string {
280	out := strings.Split(text, "\n")
281	// Split will include a "" after the last line. Remove it.
282	if n := len(out) - 1; n >= 0 && out[n] == "" {
283		out = out[:n]
284	}
285	return out
286}
287
288// nonBlankLines returns the non-blank lines in text.
289func nonBlankLines(text string) []string {
290	var out []string
291	for _, s := range lines(text) {
292		if strings.TrimSpace(s) != "" {
293			out = append(out, s)
294		}
295	}
296	return out
297}
298
299func commandString(command string, args []string) string {
300	return strings.Join(append([]string{command}, args...), " ")
301}
302
303func dief(format string, args ...interface{}) {
304	printf(format, args...)
305	exit(1)
306}
307
308var exitTrap func()
309
310func exit(code int) {
311	if exitTrap != nil {
312		exitTrap()
313	}
314	os.Exit(code)
315}
316
317func verbosef(format string, args ...interface{}) {
318	if *verbose > 0 {
319		printf(format, args...)
320	}
321}
322
323var stdoutTrap, stderrTrap *bytes.Buffer
324
325func stdout() io.Writer {
326	if stdoutTrap != nil {
327		return stdoutTrap
328	}
329	return os.Stdout
330}
331
332func stderr() io.Writer {
333	if stderrTrap != nil {
334		return stderrTrap
335	}
336	return os.Stderr
337}
338
339func printf(format string, args ...interface{}) {
340	fmt.Fprintf(stderr(), "%s: %s\n", progName, fmt.Sprintf(format, args...))
341}
342
343// count is a flag.Value that is like a flag.Bool and a flag.Int.
344// If used as -name, it increments the count, but -name=x sets the count.
345// Used for verbose flag -v.
346type count int
347
348func (c *count) String() string {
349	return fmt.Sprint(int(*c))
350}
351
352func (c *count) Set(s string) error {
353	switch s {
354	case "true":
355		*c++
356	case "false":
357		*c = 0
358	default:
359		n, err := strconv.Atoi(s)
360		if err != nil {
361			return fmt.Errorf("invalid count %q", s)
362		}
363		*c = count(n)
364	}
365	return nil
366}
367
368func (c *count) IsBoolFlag() bool {
369	return true
370}
371