github.com/golang/review@v0.0.0-20190122205339-266ee1edf5c3/git-codereview/branch.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 "net/url" 11 "os" 12 "os/exec" 13 "regexp" 14 "runtime" 15 "strings" 16 ) 17 18 // Branch describes a Git branch. 19 type Branch struct { 20 Name string // branch name 21 loadedPending bool // following fields are valid 22 originBranch string // upstream origin branch 23 commitsAhead int // number of commits ahead of origin branch 24 branchpoint string // latest commit hash shared with origin branch 25 pending []*Commit // pending commits, newest first (children before parents) 26 } 27 28 // A Commit describes a single pending commit on a Git branch. 29 type Commit struct { 30 Hash string // commit hash 31 ShortHash string // abbreviated commit hash 32 Parent string // parent hash 33 Merge string // for merges, hash of commit being merged into Parent 34 Message string // commit message 35 Subject string // first line of commit message 36 ChangeID string // Change-Id in commit message ("" if missing) 37 38 // For use by pending command. 39 g *GerritChange // associated Gerrit change data 40 gerr error // error loading Gerrit data 41 committed []string // list of files in this commit 42 } 43 44 // CurrentBranch returns the current branch. 45 func CurrentBranch() *Branch { 46 name := strings.TrimPrefix(trim(cmdOutput("git", "rev-parse", "--abbrev-ref", "HEAD")), "heads/") 47 return &Branch{Name: name} 48 } 49 50 // DetachedHead reports whether branch b corresponds to a detached HEAD 51 // (does not have a real branch name). 52 func (b *Branch) DetachedHead() bool { 53 return b.Name == "HEAD" 54 } 55 56 // OriginBranch returns the name of the origin branch that branch b tracks. 57 // The returned name is like "origin/master" or "origin/dev.garbage" or 58 // "origin/release-branch.go1.4". 59 func (b *Branch) OriginBranch() string { 60 if b.DetachedHead() { 61 // Detached head mode. 62 // "origin/HEAD" is clearly false, but it should be easy to find when it 63 // appears in other commands. Really any caller of OriginBranch 64 // should check for detached head mode. 65 return "origin/HEAD" 66 } 67 68 if b.originBranch != "" { 69 return b.originBranch 70 } 71 argv := []string{"git", "rev-parse", "--abbrev-ref", b.Name + "@{u}"} 72 cmd := exec.Command(argv[0], argv[1:]...) 73 if runtime.GOOS == "windows" { 74 // Workaround on windows. git for windows can't handle @{u} as same as 75 // given. Disable glob for this command if running on Cygwin or MSYS2. 76 envs := os.Environ() 77 envs = append(envs, "CYGWIN=noglob "+os.Getenv("CYGWIN")) 78 envs = append(envs, "MSYS=noglob "+os.Getenv("MSYS")) 79 cmd.Env = envs 80 } 81 82 out, err := cmd.CombinedOutput() 83 if err == nil && len(out) > 0 { 84 b.originBranch = string(bytes.TrimSpace(out)) 85 return b.originBranch 86 } 87 88 // Have seen both "No upstream configured" and "no upstream configured". 89 if strings.Contains(string(out), "upstream configured") { 90 // Assume branch was created before we set upstream correctly. 91 b.originBranch = "origin/master" 92 return b.originBranch 93 } 94 fmt.Fprintf(stderr(), "%v\n%s\n", commandString(argv[0], argv[1:]), out) 95 dief("%v", err) 96 panic("not reached") 97 } 98 99 func (b *Branch) FullName() string { 100 if b.Name != "HEAD" { 101 return "refs/heads/" + b.Name 102 } 103 return b.Name 104 } 105 106 // IsLocalOnly reports whether b is a local work branch (only local, not known to remote server). 107 func (b *Branch) IsLocalOnly() bool { 108 return "origin/"+b.Name != b.OriginBranch() 109 } 110 111 // HasPendingCommit reports whether b has any pending commits. 112 func (b *Branch) HasPendingCommit() bool { 113 b.loadPending() 114 return b.commitsAhead > 0 115 } 116 117 // Pending returns b's pending commits, newest first (children before parents). 118 func (b *Branch) Pending() []*Commit { 119 b.loadPending() 120 return b.pending 121 } 122 123 // Branchpoint returns an identifier for the latest revision 124 // common to both this branch and its upstream branch. 125 func (b *Branch) Branchpoint() string { 126 b.loadPending() 127 return b.branchpoint 128 } 129 130 func (b *Branch) loadPending() { 131 if b.loadedPending { 132 return 133 } 134 b.loadedPending = true 135 136 // In case of early return. 137 b.branchpoint = trim(cmdOutput("git", "rev-parse", "HEAD")) 138 139 if b.DetachedHead() { 140 return 141 } 142 143 // Note: This runs in parallel with "git fetch -q", 144 // so the commands may see a stale version of origin/master. 145 // The use of origin here is for identifying what the branch has 146 // in common with origin (what's old on the branch). 147 // Any new commits in origin do not affect that. 148 149 // Note: --topo-order means child first, then parent. 150 origin := b.OriginBranch() 151 const numField = 5 152 all := trim(cmdOutput("git", "log", "--topo-order", "--format=format:%H%x00%h%x00%P%x00%B%x00%s%x00", origin+".."+b.FullName(), "--")) 153 fields := strings.Split(all, "\x00") 154 if len(fields) < numField { 155 return // nothing pending 156 } 157 for i, field := range fields { 158 fields[i] = strings.TrimLeft(field, "\r\n") 159 } 160 foundMergeBranchpoint := false 161 for i := 0; i+numField <= len(fields); i += numField { 162 c := &Commit{ 163 Hash: fields[i], 164 ShortHash: fields[i+1], 165 Parent: strings.TrimSpace(fields[i+2]), // %P starts with \n for some reason 166 Message: fields[i+3], 167 Subject: fields[i+4], 168 } 169 if j := strings.Index(c.Parent, " "); j >= 0 { 170 c.Parent, c.Merge = c.Parent[:j], c.Parent[j+1:] 171 // Found merge point. 172 // Merges break the invariant that the last shared commit (the branchpoint) 173 // is the parent of the final commit in the log output. 174 // If c.Parent is on the origin branch, then since we are reading the log 175 // in (reverse) topological order, we know that c.Parent is the actual branchpoint, 176 // even if we later see additional commits on a different branch leading down to 177 // a lower location on the same origin branch. 178 // Check c.Merge (the second parent) too, so we don't depend on the parent order. 179 if strings.Contains(cmdOutput("git", "branch", "-a", "--contains", c.Parent), " "+origin+"\n") { 180 foundMergeBranchpoint = true 181 b.branchpoint = c.Parent 182 } 183 if strings.Contains(cmdOutput("git", "branch", "-a", "--contains", c.Merge), " "+origin+"\n") { 184 foundMergeBranchpoint = true 185 b.branchpoint = c.Merge 186 } 187 } 188 for _, line := range lines(c.Message) { 189 // Note: Keep going even if we find one, so that 190 // we take the last Change-Id line, just in case 191 // there is a commit message quoting another 192 // commit message. 193 // I'm not sure this can come up at all, but just in case. 194 if strings.HasPrefix(line, "Change-Id: ") { 195 c.ChangeID = line[len("Change-Id: "):] 196 } 197 } 198 199 b.pending = append(b.pending, c) 200 if !foundMergeBranchpoint { 201 b.branchpoint = c.Parent 202 } 203 } 204 b.commitsAhead = len(b.pending) 205 } 206 207 // CommitsBehind reports the number of commits present upstream 208 // that are not present in the current branch. 209 func (b *Branch) CommitsBehind() int { 210 return len(lines(cmdOutput("git", "log", "--format=format:x", b.FullName()+".."+b.OriginBranch(), "--"))) 211 } 212 213 // Submitted reports whether some form of b's pending commit 214 // has been cherry picked to origin. 215 func (b *Branch) Submitted(id string) bool { 216 if id == "" { 217 return false 218 } 219 line := "Change-Id: " + id 220 out := cmdOutput("git", "log", "-n", "1", "-F", "--grep", line, b.Name+".."+b.OriginBranch(), "--") 221 return strings.Contains(out, line) 222 } 223 224 var stagedRE = regexp.MustCompile(`^[ACDMR] `) 225 226 // HasStagedChanges reports whether the working directory contains staged changes. 227 func HasStagedChanges() bool { 228 for _, s := range nonBlankLines(cmdOutput("git", "status", "-b", "--porcelain")) { 229 if stagedRE.MatchString(s) { 230 return true 231 } 232 } 233 return false 234 } 235 236 var unstagedRE = regexp.MustCompile(`^.[ACDMR]`) 237 238 // HasUnstagedChanges reports whether the working directory contains unstaged changes. 239 func HasUnstagedChanges() bool { 240 for _, s := range nonBlankLines(cmdOutput("git", "status", "-b", "--porcelain")) { 241 if unstagedRE.MatchString(s) { 242 return true 243 } 244 } 245 return false 246 } 247 248 // LocalChanges returns a list of files containing staged, unstaged, and untracked changes. 249 // The elements of the returned slices are typically file names, always relative to the root, 250 // but there are a few alternate forms. First, for renaming or copying, the element takes 251 // the form `from -> to`. Second, in the case of files with names that contain unusual characters, 252 // the files (or the from, to fields of a rename or copy) are quoted C strings. 253 // For now, we expect the caller only shows these to the user, so these exceptions are okay. 254 func LocalChanges() (staged, unstaged, untracked []string) { 255 for _, s := range lines(cmdOutput("git", "status", "-b", "--porcelain")) { 256 if len(s) < 4 || s[2] != ' ' { 257 continue 258 } 259 switch s[0] { 260 case 'A', 'C', 'D', 'M', 'R': 261 staged = append(staged, s[3:]) 262 case '?': 263 untracked = append(untracked, s[3:]) 264 } 265 switch s[1] { 266 case 'A', 'C', 'D', 'M', 'R': 267 unstaged = append(unstaged, s[3:]) 268 } 269 } 270 return 271 } 272 273 // LocalBranches returns a list of all known local branches. 274 // If the current directory is in detached HEAD mode, one returned 275 // branch will have Name == "HEAD" and DetachedHead() == true. 276 func LocalBranches() []*Branch { 277 var branches []*Branch 278 current := CurrentBranch() 279 for _, s := range nonBlankLines(cmdOutput("git", "branch", "-q")) { 280 s = strings.TrimSpace(s) 281 if strings.HasPrefix(s, "* ") { 282 // * marks current branch in output. 283 // Normally the current branch has a name like any other, 284 // but in detached HEAD mode the branch listing shows 285 // a localized (translated) textual description instead of 286 // a branch name. Avoid language-specific differences 287 // by using CurrentBranch().Name for the current branch. 288 // It detects detached HEAD mode in a more portable way. 289 // (git rev-parse --abbrev-ref HEAD returns 'HEAD'). 290 s = current.Name 291 } 292 branches = append(branches, &Branch{Name: s}) 293 } 294 return branches 295 } 296 297 func OriginBranches() []string { 298 var branches []string 299 for _, line := range nonBlankLines(cmdOutput("git", "branch", "-a", "-q")) { 300 line = strings.TrimSpace(line) 301 if i := strings.Index(line, " -> "); i >= 0 { 302 line = line[:i] 303 } 304 name := strings.TrimSpace(strings.TrimPrefix(line, "* ")) 305 if strings.HasPrefix(name, "remotes/origin/") { 306 branches = append(branches, strings.TrimPrefix(name, "remotes/")) 307 } 308 } 309 return branches 310 } 311 312 // GerritChange returns the change metadata from the Gerrit server 313 // for the branch's pending change. 314 // The extra strings are passed to the Gerrit API request as o= parameters, 315 // to enable additional information. Typical values include "LABELS" and "CURRENT_REVISION". 316 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html for details. 317 func (b *Branch) GerritChange(c *Commit, extra ...string) (*GerritChange, error) { 318 if !b.HasPendingCommit() { 319 return nil, fmt.Errorf("no changes pending") 320 } 321 if c.ChangeID == "" { 322 return nil, fmt.Errorf("missing Change-Id") 323 } 324 id := fullChangeID(b, c) 325 for i, x := range extra { 326 if i == 0 { 327 id += "?" 328 } else { 329 id += "&" 330 } 331 id += "o=" + x 332 } 333 return readGerritChange(id) 334 } 335 336 // GerritChange returns the change metadata from the Gerrit server 337 // for the given changes, which each be be the result of fullChangeID(b, c) for some c. 338 // The extra strings are passed to the Gerrit API request as o= parameters, 339 // to enable additional information. Typical values include "LABELS" and "CURRENT_REVISION". 340 // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html for details. 341 func (b *Branch) GerritChanges(ids []string, extra ...string) ([][]*GerritChange, error) { 342 q := "" 343 for _, id := range ids { 344 if q != "" { 345 q += "&" 346 } 347 if strings.HasSuffix(id, "~") { 348 // result of fullChangeID(b, c) with missing Change-Id; don't send 349 q += "q=is:closed+is:open" // cannot match anything 350 continue 351 } 352 q += "q=change:" + url.QueryEscape(id) 353 } 354 if q == "" { 355 return nil, fmt.Errorf("no changes found") 356 } 357 for _, x := range extra { 358 q += "&o=" + url.QueryEscape(x) 359 } 360 return readGerritChanges(q) 361 } 362 363 // CommitByRev finds a unique pending commit by its git <rev>. 364 // It dies if rev cannot be resolved to a commit or that commit is not 365 // pending on b using the action ("mail", "submit") in the failure message. 366 func (b *Branch) CommitByRev(action, rev string) *Commit { 367 // Parse rev to a commit hash. 368 hash, err := cmdOutputErr("git", "rev-parse", "--verify", rev+"^{commit}") 369 if err != nil { 370 msg := strings.TrimPrefix(trim(err.Error()), "fatal: ") 371 dief("cannot %s: %s", action, msg) 372 } 373 hash = trim(hash) 374 375 // Check that hash is a pending commit. 376 var c *Commit 377 for _, c1 := range b.Pending() { 378 if c1.Hash == hash { 379 c = c1 380 break 381 } 382 } 383 if c == nil { 384 dief("cannot %s: commit hash %q not found in the current branch", action, hash) 385 } 386 return c 387 } 388 389 // DefaultCommit returns the default pending commit for this branch. 390 // It dies if there is not exactly one pending commit, 391 // using the action (e.g. "mail", "submit") and optional extra instructions 392 // in the failure message. 393 func (b *Branch) DefaultCommit(action, extra string) *Commit { 394 work := b.Pending() 395 if len(work) == 0 { 396 dief("cannot %s: no changes pending", action) 397 } 398 if len(work) >= 2 { 399 var buf bytes.Buffer 400 for _, c := range work { 401 fmt.Fprintf(&buf, "\n\t%s %s", c.ShortHash, c.Subject) 402 } 403 if extra != "" { 404 extra = "; " + extra 405 } 406 dief("cannot %s: multiple changes pending%s:%s", action, extra, buf.String()) 407 } 408 return work[0] 409 } 410 411 // ListFiles returns the list of files in a given commit. 412 func ListFiles(c *Commit) []string { 413 return nonBlankLines(cmdOutput("git", "diff", "--name-only", c.Parent, c.Hash, "--")) 414 } 415 416 func cmdBranchpoint(args []string) { 417 expectZeroArgs(args, "sync") 418 fmt.Fprintf(stdout(), "%s\n", CurrentBranch().Branchpoint()) 419 } 420 421 func cmdRebaseWork(args []string) { 422 expectZeroArgs(args, "rebase-work") 423 b := CurrentBranch() 424 if HasStagedChanges() || HasUnstagedChanges() { 425 dief("cannot rebase with uncommitted work") 426 } 427 if len(b.Pending()) == 0 { 428 dief("no pending work") 429 } 430 run("git", "rebase", "-i", b.Branchpoint()) 431 }