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 }