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