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  }