github.com/golang/review@v0.0.0-20190122205339-266ee1edf5c3/git-codereview/change.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  	"fmt"
     9  	"os"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  )
    14  
    15  var commitMsg string
    16  var changeAuto bool
    17  var changeQuick bool
    18  
    19  func cmdChange(args []string) {
    20  	flags.StringVar(&commitMsg, "m", "", "specify a commit message")
    21  	flags.BoolVar(&changeAuto, "a", false, "add changes to any tracked files")
    22  	flags.BoolVar(&changeQuick, "q", false, "do not edit pending commit msg")
    23  	flags.Parse(args)
    24  	if len(flags.Args()) > 1 {
    25  		fmt.Fprintf(stderr(), "Usage: %s change %s [branch]\n", os.Args[0], globalFlags)
    26  		os.Exit(2)
    27  	}
    28  
    29  	// Checkout or create branch, if specified.
    30  	target := flags.Arg(0)
    31  	if target != "" {
    32  		checkoutOrCreate(target)
    33  		b := CurrentBranch()
    34  		if HasStagedChanges() && b.IsLocalOnly() && !b.HasPendingCommit() {
    35  			commitChanges(false)
    36  		}
    37  		b.check()
    38  		return
    39  	}
    40  
    41  	// Create or amend change commit.
    42  	b := CurrentBranch()
    43  	if !b.IsLocalOnly() {
    44  		dief("can't commit to %s branch (use '%s change branchname').", b.Name, os.Args[0])
    45  	}
    46  
    47  	amend := b.HasPendingCommit()
    48  	if amend {
    49  		// Dies if there is not exactly one commit.
    50  		b.DefaultCommit("amend change", "")
    51  	}
    52  	commitChanges(amend)
    53  	b.loadedPending = false // force reload after commitChanges
    54  	b.check()
    55  }
    56  
    57  func (b *Branch) check() {
    58  	// TODO(rsc): Test
    59  	staged, unstaged, _ := LocalChanges()
    60  	if len(staged) == 0 && len(unstaged) == 0 {
    61  		// No staged changes, no unstaged changes.
    62  		// If the branch is behind upstream, now is a good time to point that out.
    63  		// This applies to both local work branches and tracking branches.
    64  		// TODO(rsc): Test.
    65  		b.loadPending()
    66  		if n := b.CommitsBehind(); n > 0 {
    67  			printf("warning: %d commit%s behind %s; run 'git codereview sync' to update.", n, suffix(n, "s"), b.OriginBranch())
    68  		}
    69  	}
    70  
    71  	// TODO(rsc): Test
    72  	if text := b.errors(); text != "" {
    73  		printf("error: %s\n", text)
    74  	}
    75  }
    76  
    77  var testCommitMsg string
    78  
    79  func commitChanges(amend bool) {
    80  	// git commit will run the gofmt hook.
    81  	// Run it now to give a better error (won't show a git commit command failing).
    82  	hookGofmt()
    83  
    84  	if HasUnstagedChanges() && !HasStagedChanges() && !changeAuto {
    85  		printf("warning: unstaged changes and no staged changes; use 'git add' or 'git change -a'")
    86  	}
    87  	commit := func(amend bool) {
    88  		args := []string{"commit", "-q", "--allow-empty"}
    89  		if amend {
    90  			args = append(args, "--amend")
    91  			if changeQuick {
    92  				args = append(args, "--no-edit")
    93  			}
    94  		}
    95  		if commitMsg != "" {
    96  			args = append(args, "-m", commitMsg)
    97  		} else if testCommitMsg != "" {
    98  			args = append(args, "-m", testCommitMsg)
    99  		}
   100  		if changeAuto {
   101  			args = append(args, "-a")
   102  		}
   103  		run("git", args...)
   104  	}
   105  	commit(amend)
   106  	for !commitMessageOK() {
   107  		fmt.Print("re-edit commit message (y/n)? ")
   108  		if !scanYes() {
   109  			break
   110  		}
   111  		commit(true)
   112  	}
   113  	printf("change updated.")
   114  }
   115  
   116  func checkoutOrCreate(target string) {
   117  	// If it's a valid Gerrit number, checkout the CL.
   118  	cl, ps, isCL := parseCL(target)
   119  	if isCL {
   120  		if !haveGerrit() {
   121  			dief("cannot change to a CL without gerrit")
   122  		}
   123  		if HasStagedChanges() || HasUnstagedChanges() {
   124  			dief("cannot change to a CL with uncommitted work")
   125  		}
   126  		checkoutCL(cl, ps)
   127  		return
   128  	}
   129  
   130  	if strings.ToUpper(target) == "HEAD" {
   131  		// Git gets very upset and confused if you 'git change head'
   132  		// on systems with case-insensitive file names: the branch
   133  		// head conflicts with the usual HEAD.
   134  		dief("invalid branch name %q: ref name HEAD is reserved for git.", target)
   135  	}
   136  
   137  	// If local branch exists, check it out.
   138  	for _, b := range LocalBranches() {
   139  		if b.Name == target {
   140  			run("git", "checkout", "-q", target)
   141  			printf("changed to branch %v.", target)
   142  			return
   143  		}
   144  	}
   145  
   146  	// If origin branch exists, create local branch tracking it.
   147  	for _, name := range OriginBranches() {
   148  		if name == "origin/"+target {
   149  			run("git", "checkout", "-q", "-t", "-b", target, name)
   150  			printf("created branch %v tracking %s.", target, name)
   151  			return
   152  		}
   153  	}
   154  
   155  	// Otherwise, this is a request to create a local work branch.
   156  	// Check for reserved names. We take everything with a dot.
   157  	if strings.Contains(target, ".") {
   158  		dief("invalid branch name %v: branch names with dots are reserved for git-codereview.", target)
   159  	}
   160  
   161  	// If the current branch has a pending commit, building
   162  	// on top of it will not help. Don't allow that.
   163  	// Otherwise, inherit HEAD and upstream from the current branch.
   164  	b := CurrentBranch()
   165  	if b.HasPendingCommit() {
   166  		if !b.IsLocalOnly() {
   167  			dief("bad repo state: branch %s is ahead of origin/%s", b.Name, b.Name)
   168  		}
   169  		dief("cannot branch from work branch; change back to %v first.", strings.TrimPrefix(b.OriginBranch(), "origin/"))
   170  	}
   171  
   172  	origin := b.OriginBranch()
   173  
   174  	// NOTE: This is different from git checkout -q -t -b branch. It does not move HEAD.
   175  	run("git", "checkout", "-q", "-b", target)
   176  	run("git", "branch", "-q", "--set-upstream-to", origin)
   177  	printf("created branch %v tracking %s.", target, origin)
   178  }
   179  
   180  // Checkout the patch set of the given CL. When patch set is empty, use the latest.
   181  func checkoutCL(cl, ps string) {
   182  	if ps == "" {
   183  		change, err := readGerritChange(cl + "?o=CURRENT_REVISION")
   184  		if err != nil {
   185  			dief("cannot change to CL %s: %v", cl, err)
   186  		}
   187  		rev, ok := change.Revisions[change.CurrentRevision]
   188  		if !ok {
   189  			dief("cannot change to CL %s: invalid current revision from gerrit", cl)
   190  		}
   191  		ps = strconv.Itoa(rev.Number)
   192  	}
   193  
   194  	var group string
   195  	if len(cl) > 1 {
   196  		group = cl[len(cl)-2:]
   197  	} else {
   198  		group = "0" + cl
   199  	}
   200  	ref := fmt.Sprintf("refs/changes/%s/%s/%s", group, cl, ps)
   201  
   202  	err := runErr("git", "fetch", "-q", "origin", ref)
   203  	if err != nil {
   204  		dief("cannot change to CL %s/%s: %v", cl, ps, err)
   205  	}
   206  	err = runErr("git", "checkout", "-q", "FETCH_HEAD")
   207  	if err != nil {
   208  		dief("cannot change to CL %s/%s: %v", cl, ps, err)
   209  	}
   210  	subject, err := trimErr(cmdOutputErr("git", "log", "--format=%s", "-1"))
   211  	if err != nil {
   212  		printf("changed to CL %s/%s.", cl, ps)
   213  		dief("cannot read change subject from git: %v", err)
   214  	}
   215  	printf("changed to CL %s/%s.\n\t%s", cl, ps, subject)
   216  }
   217  
   218  var parseCLRE = regexp.MustCompile(`^([0-9]+)(?:/([0-9]+))?$`)
   219  
   220  // parseCL validates and splits the CL number and patch set (if present).
   221  func parseCL(arg string) (cl, patchset string, ok bool) {
   222  	m := parseCLRE.FindStringSubmatch(arg)
   223  	if len(m) == 0 {
   224  		return "", "", false
   225  	}
   226  	return m[1], m[2], true
   227  }
   228  
   229  var messageRE = regexp.MustCompile(`^(\[[a-zA-Z0-9.-]+\] )?[a-zA-Z0-9-/,. ]+: `)
   230  
   231  func commitMessageOK() bool {
   232  	body := cmdOutput("git", "log", "--format=format:%B", "-n", "1")
   233  	ok := true
   234  	if !messageRE.MatchString(body) {
   235  		fmt.Print(commitMessageWarning)
   236  		ok = false
   237  	}
   238  	return ok
   239  }
   240  
   241  const commitMessageWarning = `
   242  Your CL description appears not to use the standard form.
   243  
   244  The first line of your change description is conventionally a one-line summary
   245  of the change, prefixed by the primary affected package, and is used as the
   246  subject for code review mail; the rest of the description elaborates.
   247  
   248  Examples:
   249  
   250  	encoding/rot13: new package
   251  
   252  	math: add IsInf, IsNaN
   253  
   254  	net: fix cname in LookupHost
   255  
   256  	unicode: update to Unicode 5.0.2
   257  
   258  `
   259  
   260  func scanYes() bool {
   261  	var s string
   262  	fmt.Scan(&s)
   263  	return strings.HasPrefix(strings.ToLower(s), "y")
   264  }