| // 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. |
| |
| package main |
| |
| import ( |
| "bytes" |
| "fmt" |
| "os" |
| "strings" |
| "time" |
| ) |
| |
| // TODO(rsc): Add -tbr, along with standard exceptions (doc/go1.5.txt) |
| |
| func cmdSubmit(args []string) { |
| var interactive bool |
| flags.BoolVar(&interactive, "i", false, "interactively select commits to submit") |
| flags.Usage = func() { |
| fmt.Fprintf(stderr(), "Usage: %s submit %s [-i | commit...]\n", os.Args[0], globalFlags) |
| } |
| flags.Parse(args) |
| if interactive && flags.NArg() > 0 { |
| flags.Usage() |
| os.Exit(2) |
| } |
| |
| b := CurrentBranch() |
| var cs []*Commit |
| if interactive { |
| hashes := submitHashes(b) |
| if len(hashes) == 0 { |
| printf("nothing to submit") |
| return |
| } |
| for _, hash := range hashes { |
| cs = append(cs, b.CommitByRev("submit", hash)) |
| } |
| } else if args := flags.Args(); len(args) >= 1 { |
| for _, arg := range args { |
| cs = append(cs, b.CommitByRev("submit", arg)) |
| } |
| } else { |
| cs = append(cs, b.DefaultCommit("submit", "must specify commit on command line or use submit -i")) |
| } |
| |
| // No staged changes. |
| // Also, no unstaged changes, at least for now. |
| // This makes sure the sync at the end will work well. |
| // We can relax this later if there is a good reason. |
| checkStaged("submit") |
| checkUnstaged("submit") |
| |
| // Submit the changes. |
| var g *GerritChange |
| for _, c := range cs { |
| printf("submitting %s %s", c.ShortHash, c.Subject) |
| g = submit(b, c) |
| } |
| |
| // Sync client to revision that Gerrit committed, but only if we can do it cleanly. |
| // Otherwise require user to run 'git sync' themselves (if they care). |
| run("git", "fetch", "-q") |
| if len(cs) == 1 && len(b.Pending()) == 1 { |
| if err := runErr("git", "checkout", "-q", "-B", b.Name, g.CurrentRevision, "--"); err != nil { |
| dief("submit succeeded, but cannot sync local branch\n"+ |
| "\trun 'git sync' to sync, or\n"+ |
| "\trun 'git branch -D %s; git change master; git sync' to discard local branch", b.Name) |
| } |
| } else { |
| printf("submit succeeded; run 'git sync' to sync") |
| } |
| |
| // Done! Change is submitted, branch is up to date, ready for new work. |
| } |
| |
| // submit submits a single commit c on branch b and returns the |
| // GerritChange for the submitted change. It dies if the submit fails. |
| func submit(b *Branch, c *Commit) *GerritChange { |
| if strings.Contains(strings.ToLower(c.Message), "do not submit") { |
| dief("%s: CL says DO NOT SUBMIT", c.ShortHash) |
| } |
| |
| // Fetch Gerrit information about this change. |
| g, err := b.GerritChange(c, "LABELS", "CURRENT_REVISION") |
| if err != nil { |
| dief("%v", err) |
| } |
| |
| // Pre-check that this change appears submittable. |
| // The final submit will check this too, but it is better to fail now. |
| if err = submitCheck(g); err != nil { |
| dief("cannot submit: %v", err) |
| } |
| |
| // Upload most recent revision if not already on server. |
| |
| if c.Hash != g.CurrentRevision { |
| run("git", "push", "-q", "origin", b.PushSpec(c)) |
| |
| // Refetch change information. |
| g, err = b.GerritChange(c, "LABELS", "CURRENT_REVISION") |
| if err != nil { |
| dief("%v", err) |
| } |
| } |
| |
| if *noRun { |
| printf("stopped before submit") |
| return g |
| } |
| |
| // Otherwise, try the submit. Sends back updated GerritChange, |
| // but we need extended information and the reply is in the |
| // "SUBMITTED" state anyway, so ignore the GerritChange |
| // in the response and fetch a new one below. |
| if err := gerritAPI("/a/changes/"+fullChangeID(b, c)+"/submit", []byte(`{"wait_for_merge": true}`), nil); err != nil { |
| dief("cannot submit: %v", err) |
| } |
| |
| // It is common to get back "SUBMITTED" for a split second after the |
| // request is made. That indicates that the change has been queued for submit, |
| // but the first merge (the one wait_for_merge waited for) |
| // failed, possibly due to a spurious condition. We see this often, and the |
| // status usually changes to MERGED shortly thereafter. |
| // Wait a little while to see if we can get to a different state. |
| const steps = 6 |
| const max = 2 * time.Second |
| for i := 0; i < steps; i++ { |
| time.Sleep(max * (1 << uint(i+1)) / (1 << steps)) |
| g, err = b.GerritChange(c, "LABELS", "CURRENT_REVISION") |
| if err != nil { |
| dief("waiting for merge: %v", err) |
| } |
| if g.Status != "SUBMITTED" { |
| break |
| } |
| } |
| |
| switch g.Status { |
| default: |
| dief("submit error: unexpected post-submit Gerrit change status %q", g.Status) |
| |
| case "MERGED": |
| // good |
| |
| case "SUBMITTED": |
| // see above |
| dief("cannot submit: timed out waiting for change to be submitted by Gerrit") |
| } |
| |
| return g |
| } |
| |
| // submitCheck checks that g should be submittable. This is |
| // necessarily a best-effort check. |
| // |
| // g must have the "LABELS" option. |
| func submitCheck(g *GerritChange) error { |
| // Check Gerrit change status. |
| switch g.Status { |
| default: |
| return fmt.Errorf("unexpected Gerrit change status %q", g.Status) |
| |
| case "NEW", "SUBMITTED": |
| // Not yet "MERGED", so try the submit. |
| // "SUBMITTED" is a weird state. It means that Submit has been clicked once, |
| // but it hasn't happened yet, usually because of a merge failure. |
| // The user may have done git sync and may now have a mergable |
| // copy waiting to be uploaded, so continue on as if it were "NEW". |
| |
| case "MERGED": |
| // Can happen if moving between different clients. |
| return fmt.Errorf("change already submitted, run 'git sync'") |
| |
| case "ABANDONED": |
| return fmt.Errorf("change abandoned") |
| } |
| |
| // Check for label approvals (like CodeReview+2). |
| for _, name := range g.LabelNames() { |
| label := g.Labels[name] |
| if label.Optional { |
| continue |
| } |
| if label.Rejected != nil { |
| return fmt.Errorf("change has %s rejection", name) |
| } |
| if label.Approved == nil { |
| return fmt.Errorf("change missing %s approval", name) |
| } |
| } |
| |
| return nil |
| } |
| |
| // submitHashes interactively prompts for commits to submit. |
| func submitHashes(b *Branch) []string { |
| // Get pending commits on b. |
| pending := b.Pending() |
| for _, c := range pending { |
| // Note that DETAILED_LABELS does not imply LABELS. |
| c.g, c.gerr = b.GerritChange(c, "CURRENT_REVISION", "LABELS", "DETAILED_LABELS") |
| if c.g == nil { |
| c.g = new(GerritChange) |
| } |
| } |
| |
| // Construct submit script. |
| var script bytes.Buffer |
| for i := len(pending) - 1; i >= 0; i-- { |
| c := pending[i] |
| |
| if c.g.ID == "" { |
| fmt.Fprintf(&script, "# change not on Gerrit:\n#") |
| } else if err := submitCheck(c.g); err != nil { |
| fmt.Fprintf(&script, "# %v:\n#", err) |
| } |
| |
| formatCommit(&script, c, true) |
| } |
| |
| fmt.Fprintf(&script, ` |
| # The above commits will be submitted in order from top to bottom |
| # when you exit the editor. |
| # |
| # These lines can be re-ordered, removed, and commented out. |
| # |
| # If you remove all lines, the submit will be aborted. |
| `) |
| |
| // Edit the script. |
| final := editor(script.String()) |
| |
| // Parse the final script. |
| var hashes []string |
| for _, line := range lines(final) { |
| line := strings.TrimSpace(line) |
| if len(line) == 0 || line[0] == '#' { |
| continue |
| } |
| if i := strings.Index(line, " "); i >= 0 { |
| line = line[:i] |
| } |
| hashes = append(hashes, line) |
| } |
| |
| return hashes |
| } |