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