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
5package main
6
7import (
8	"bytes"
9	"fmt"
10	"strings"
11	"time"
12)
13
14func cmdSubmit(args []string) {
15	// NOTE: New flags should be added to the usage message below as well as doc.go.
16	var interactive bool
17	flags.BoolVar(&interactive, "i", false, "interactively select commits to submit")
18	flags.Usage = func() {
19		fmt.Fprintf(stderr(), "Usage: %s submit %s [-i | commit...]\n", progName, globalFlags)
20		exit(2)
21	}
22	flags.Parse(args)
23	if interactive && flags.NArg() > 0 {
24		flags.Usage()
25		exit(2)
26	}
27
28	b := CurrentBranch()
29	var cs []*Commit
30	if interactive {
31		hashes := submitHashes(b)
32		if len(hashes) == 0 {
33			printf("nothing to submit")
34			return
35		}
36		for _, hash := range hashes {
37			cs = append(cs, b.CommitByRev("submit", hash))
38		}
39	} else if args := flags.Args(); len(args) >= 1 {
40		for _, arg := range args {
41			cs = append(cs, b.CommitByRev("submit", arg))
42		}
43	} else {
44		cs = append(cs, b.DefaultCommit("submit", "must specify commit on command line or use submit -i"))
45	}
46
47	// No staged changes.
48	// Also, no unstaged changes, at least for now.
49	// This makes sure the sync at the end will work well.
50	// We can relax this later if there is a good reason.
51	checkStaged("submit")
52	checkUnstaged("submit")
53
54	// Submit the changes.
55	var g *GerritChange
56	for _, c := range cs {
57		printf("submitting %s %s", c.ShortHash, c.Subject)
58		g = submit(b, c)
59	}
60
61	// Sync client to revision that Gerrit committed, but only if we can do it cleanly.
62	// Otherwise require user to run 'git sync' themselves (if they care).
63	run("git", "fetch", "-q")
64	if len(cs) == 1 && len(b.Pending()) == 1 {
65		if err := runErr("git", "checkout", "-q", "-B", b.Name, g.CurrentRevision, "--"); err != nil {
66			dief("submit succeeded, but cannot sync local branch\n"+
67				"\trun 'git sync' to sync, or\n"+
68				"\trun 'git branch -D %s; git change master; git sync' to discard local branch", b.Name)
69		}
70	} else {
71		printf("submit succeeded; run 'git sync' to sync")
72	}
73
74	// Done! Change is submitted, branch is up to date, ready for new work.
75}
76
77// submit submits a single commit c on branch b and returns the
78// GerritChange for the submitted change. It dies if the submit fails.
79func submit(b *Branch, c *Commit) *GerritChange {
80	if strings.Contains(strings.ToLower(c.Message), "do not submit") {
81		dief("%s: CL says DO NOT SUBMIT", c.ShortHash)
82	}
83
84	// Fetch Gerrit information about this change.
85	g, err := b.GerritChange(c, "LABELS", "CURRENT_REVISION")
86	if err != nil {
87		dief("%v", err)
88	}
89
90	// Pre-check that this change appears submittable.
91	// The final submit will check this too, but it is better to fail now.
92	if err = submitCheck(g); err != nil {
93		dief("cannot submit: %v", err)
94	}
95
96	// Upload most recent revision if not already on server.
97
98	if c.Hash != g.CurrentRevision {
99		run("git", "push", "-q", "origin", b.PushSpec(c))
100
101		// Refetch change information.
102		g, err = b.GerritChange(c, "LABELS", "CURRENT_REVISION")
103		if err != nil {
104			dief("%v", err)
105		}
106	}
107
108	if *noRun {
109		printf("stopped before submit")
110		return g
111	}
112
113	// Otherwise, try the submit. Sends back updated GerritChange,
114	// but we need extended information and the reply is in the
115	// "SUBMITTED" state anyway, so ignore the GerritChange
116	// in the response and fetch a new one below.
117	if err := gerritAPI("/a/changes/"+fullChangeID(b, c)+"/submit", []byte(`{"wait_for_merge": true}`), nil); err != nil {
118		dief("cannot submit: %v", err)
119	}
120
121	// It is common to get back "SUBMITTED" for a split second after the
122	// request is made. That indicates that the change has been queued for submit,
123	// but the first merge (the one wait_for_merge waited for)
124	// failed, possibly due to a spurious condition. We see this often, and the
125	// status usually changes to MERGED shortly thereafter.
126	// Wait a little while to see if we can get to a different state.
127	const steps = 6
128	const max = 2 * time.Second
129	for i := 0; i < steps; i++ {
130		time.Sleep(max * (1 << uint(i+1)) / (1 << steps))
131		g, err = b.GerritChange(c, "LABELS", "CURRENT_REVISION")
132		if err != nil {
133			dief("waiting for merge: %v", err)
134		}
135		if g.Status != "SUBMITTED" {
136			break
137		}
138	}
139
140	switch g.Status {
141	default:
142		dief("submit error: unexpected post-submit Gerrit change status %q", g.Status)
143
144	case "MERGED":
145		// good
146
147	case "SUBMITTED":
148		// see above
149		dief("cannot submit: timed out waiting for change to be submitted by Gerrit")
150	}
151
152	return g
153}
154
155// submitCheck checks that g should be submittable. This is
156// necessarily a best-effort check.
157//
158// g must have the "LABELS" option.
159func submitCheck(g *GerritChange) error {
160	// Check Gerrit change status.
161	switch g.Status {
162	default:
163		return fmt.Errorf("unexpected Gerrit change status %q", g.Status)
164
165	case "NEW", "SUBMITTED":
166		// Not yet "MERGED", so try the submit.
167		// "SUBMITTED" is a weird state. It means that Submit has been clicked once,
168		// but it hasn't happened yet, usually because of a merge failure.
169		// The user may have done git sync and may now have a mergable
170		// copy waiting to be uploaded, so continue on as if it were "NEW".
171
172	case "MERGED":
173		// Can happen if moving between different clients.
174		return fmt.Errorf("change already submitted, run 'git sync'")
175
176	case "ABANDONED":
177		return fmt.Errorf("change abandoned")
178	}
179
180	// Check for label approvals (like CodeReview+2).
181	for _, name := range g.LabelNames() {
182		label := g.Labels[name]
183		if label.Optional {
184			continue
185		}
186		if label.Rejected != nil {
187			return fmt.Errorf("change has %s rejection", name)
188		}
189		if label.Approved == nil {
190			return fmt.Errorf("change missing %s approval", name)
191		}
192	}
193
194	return nil
195}
196
197// submitHashes interactively prompts for commits to submit.
198func submitHashes(b *Branch) []string {
199	// Get pending commits on b.
200	pending := b.Pending()
201	for _, c := range pending {
202		// Note that DETAILED_LABELS does not imply LABELS.
203		c.g, c.gerr = b.GerritChange(c, "CURRENT_REVISION", "LABELS", "DETAILED_LABELS")
204		if c.g == nil {
205			c.g = new(GerritChange)
206		}
207	}
208
209	// Construct submit script.
210	var script bytes.Buffer
211	for i := len(pending) - 1; i >= 0; i-- {
212		c := pending[i]
213
214		if c.g.ID == "" {
215			fmt.Fprintf(&script, "# change not on Gerrit:\n#")
216		} else if err := submitCheck(c.g); err != nil {
217			fmt.Fprintf(&script, "# %v:\n#", err)
218		}
219
220		formatCommit(&script, c, true)
221	}
222
223	fmt.Fprintf(&script, `
224# The above commits will be submitted in order from top to bottom
225# when you exit the editor.
226#
227# These lines can be re-ordered, removed, and commented out.
228#
229# If you remove all lines, the submit will be aborted.
230`)
231
232	// Edit the script.
233	final := editor(script.String())
234
235	// Parse the final script.
236	var hashes []string
237	for _, line := range lines(final) {
238		line := strings.TrimSpace(line)
239		if len(line) == 0 || line[0] == '#' {
240			continue
241		}
242		if i := strings.Index(line, " "); i >= 0 {
243			line = line[:i]
244		}
245		hashes = append(hashes, line)
246	}
247
248	return hashes
249}
250