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 5package main 6 7import ( 8 "fmt" 9 "os" 10 "path/filepath" 11 "regexp" 12 "strconv" 13 "strings" 14) 15 16var commitMsg string 17var changeAuto bool 18var changeQuick bool 19 20func cmdChange(args []string) { 21 // NOTE: New flags should be added to the usage message below as well as doc.go. 22 flags.StringVar(&commitMsg, "m", "", "specify a commit message") 23 flags.BoolVar(&changeAuto, "a", false, "add changes to any tracked files") 24 flags.BoolVar(&changeQuick, "q", false, "do not edit pending commit msg") 25 flags.Parse(args) 26 if len(flags.Args()) > 1 { 27 fmt.Fprintf(stderr(), "Usage: %s change %s [-a] [-m msg] [-q] [branch]\n", progName, globalFlags) 28 exit(2) 29 } 30 31 if _, err := cmdOutputErr("git", "rev-parse", "--abbrev-ref", "MERGE_HEAD"); err == nil { 32 diePendingMerge("change") 33 } 34 // Note: A rebase with a conflict + rebase --continue sometimes leaves behind REBASE_HEAD. 35 // So check for the rebase-merge directory instead, which it does a better job cleaning up. 36 if _, err := os.Stat(filepath.Join(gitPathDir(), "rebase-merge")); err == nil { 37 dief("cannot change: found pending rebase or sync") 38 } 39 40 // Checkout or create branch, if specified. 41 target := flags.Arg(0) 42 if target != "" { 43 checkoutOrCreate(target) 44 b := CurrentBranch() 45 if HasStagedChanges() && !b.HasPendingCommit() { 46 commitChanges(false) 47 } 48 b.check() 49 return 50 } 51 52 // Create or amend change commit. 53 b := CurrentBranch() 54 amend := b.HasPendingCommit() 55 if amend { 56 // Dies if there is not exactly one commit. 57 b.DefaultCommit("amend change", "") 58 } 59 commitChanges(amend) 60 b.loadedPending = false // force reload after commitChanges 61 b.check() 62} 63 64func (b *Branch) check() { 65 staged, unstaged, _ := LocalChanges() 66 if len(staged) == 0 && len(unstaged) == 0 { 67 // No staged changes, no unstaged changes. 68 // If the branch is behind upstream, now is a good time to point that out. 69 // This applies to both local work branches and tracking branches. 70 b.loadPending() 71 if n := b.CommitsBehind(); n > 0 { 72 printf("warning: %d commit%s behind %s; run 'git codereview sync' to update.", n, suffix(n, "s"), b.OriginBranch()) 73 } 74 } 75} 76 77var testCommitMsg string 78 79func 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 116func checkoutOrCreate(target string) { 117 // If it's a valid Gerrit number CL or CL/PS or GitHub pull request number PR, 118 // checkout the CL or PR. 119 cl, ps, isCL := parseCL(target) 120 if isCL { 121 what := "CL" 122 if !haveGerrit() && haveGitHub() { 123 what = "PR" 124 if ps != "" { 125 dief("change PR syntax is NNN not NNN.PP") 126 } 127 } 128 if what == "CL" && !haveGerrit() { 129 dief("cannot change to a CL without gerrit") 130 } 131 if HasStagedChanges() || HasUnstagedChanges() { 132 dief("cannot change to a %s with uncommitted work", what) 133 } 134 checkoutCL(what, cl, ps) 135 return 136 } 137 138 if strings.ToUpper(target) == "HEAD" { 139 // Git gets very upset and confused if you 'git change head' 140 // on systems with case-insensitive file names: the branch 141 // head conflicts with the usual HEAD. 142 dief("invalid branch name %q: ref name HEAD is reserved for git.", target) 143 } 144 145 // If local branch exists, check it out. 146 for _, b := range LocalBranches() { 147 if b.Name == target { 148 run("git", "checkout", "-q", target) 149 printf("changed to branch %v.", target) 150 return 151 } 152 } 153 154 // If origin branch exists, create local branch tracking it. 155 for _, name := range OriginBranches() { 156 if name == "origin/"+target { 157 run("git", "checkout", "-q", "-t", "-b", target, name) 158 printf("created branch %v tracking %s.", target, name) 159 return 160 } 161 } 162 163 // Otherwise, this is a request to create a local work branch. 164 // Check for reserved names. We take everything with a dot. 165 if strings.Contains(target, ".") { 166 dief("invalid branch name %v: branch names with dots are reserved for git-codereview.", target) 167 } 168 169 // If the current branch has a pending commit, building 170 // on top of it will not help. Don't allow that. 171 // Otherwise, inherit branchpoint and upstream from the current branch. 172 b := CurrentBranch() 173 branchpoint := "HEAD" 174 if b.HasPendingCommit() { 175 fmt.Fprintf(stderr(), "warning: pending changes on %s are not copied to new branch %s\n", b.Name, target) 176 branchpoint = b.Branchpoint() 177 } 178 179 origin := b.OriginBranch() 180 181 // NOTE: This is different from git checkout -q -t -b origin, 182 // because the -t wold use the origin directly, and that may be 183 // ahead of the current directory. The goal of this command is 184 // to create a new branch for work on the current directory, 185 // not to incorporate new commits at the same time (use 'git sync' for that). 186 // The ideal is that HEAD doesn't change at all. 187 // In the absence of pending commits, that ideal is achieved. 188 // But if there are pending commits, it'd be too confusing to have them 189 // on two different work branches, so we drop them and use the 190 // branchpoint they started from (after warning above), moving HEAD 191 // as little as possible. 192 run("git", "checkout", "-q", "-b", target, branchpoint) 193 run("git", "branch", "-q", "--set-upstream-to", origin) 194 printf("created branch %v tracking %s.", target, origin) 195} 196 197// Checkout the patch set of the given CL. When patch set is empty, use the latest. 198func checkoutCL(what, cl, ps string) { 199 if what == "CL" && ps == "" { 200 change, err := readGerritChange(cl + "?o=CURRENT_REVISION") 201 if err != nil { 202 dief("cannot change to CL %s: %v", cl, err) 203 } 204 rev, ok := change.Revisions[change.CurrentRevision] 205 if !ok { 206 dief("cannot change to CL %s: invalid current revision from gerrit", cl) 207 } 208 ps = strconv.Itoa(rev.Number) 209 } 210 211 var ref string 212 if what == "CL" { 213 var group string 214 if len(cl) > 1 { 215 group = cl[len(cl)-2:] 216 } else { 217 group = "0" + cl 218 } 219 cl = fmt.Sprintf("%s/%s", cl, ps) 220 ref = fmt.Sprintf("refs/changes/%s/%s", group, cl) 221 } else { 222 ref = fmt.Sprintf("pull/%s/head", cl) 223 } 224 err := runErr("git", "fetch", "-q", "origin", ref) 225 if err != nil { 226 dief("cannot change to %v %s: %v", what, cl, err) 227 } 228 err = runErr("git", "checkout", "-q", "FETCH_HEAD") 229 if err != nil { 230 dief("cannot change to %s %s: %v", what, cl, err) 231 } 232 if *noRun { 233 return 234 } 235 subject, err := trimErr(cmdOutputErr("git", "log", "--format=%s", "-1")) 236 if err != nil { 237 printf("changed to %s %s.", what, cl) 238 dief("cannot read change subject from git: %v", err) 239 } 240 printf("changed to %s %s.\n\t%s", what, cl, subject) 241} 242 243var parseCLRE = regexp.MustCompile(`^([0-9]+)(?:/([0-9]+))?$`) 244 245// parseCL validates and splits the CL number and patch set (if present). 246func parseCL(arg string) (cl, patchset string, ok bool) { 247 m := parseCLRE.FindStringSubmatch(arg) 248 if len(m) == 0 { 249 return "", "", false 250 } 251 return m[1], m[2], true 252} 253 254var messageRE = regexp.MustCompile(`^(\[[a-zA-Z0-9.-]+\] )?[a-zA-Z0-9-/,. ]+: `) 255 256func commitMessageOK() bool { 257 body := cmdOutput("git", "log", "--format=format:%B", "-n", "1") 258 ok := true 259 if !messageRE.MatchString(body) { 260 fmt.Print(commitMessageWarning) 261 ok = false 262 } 263 return ok 264} 265 266const commitMessageWarning = ` 267Your CL description appears not to use the standard form. 268 269The first line of your change description is conventionally a one-line summary 270of the change, prefixed by the primary affected package, and is used as the 271subject for code review mail; the rest of the description elaborates. 272 273Examples: 274 275 encoding/rot13: new package 276 277 math: add IsInf, IsNaN 278 279 net: fix cname in LookupHost 280 281 unicode: update to Unicode 5.0.2 282 283` 284 285func scanYes() bool { 286 var s string 287 fmt.Scan(&s) 288 return strings.HasPrefix(strings.ToLower(s), "y") 289} 290