github.com/atlassian/git-lob@v0.0.0-20150806085256-2386a5ed291a/core/git.go (about) 1 package core 2 3 import ( 4 "bufio" 5 "errors" 6 "fmt" 7 "io" 8 "os/exec" 9 "regexp" 10 "sort" 11 "strings" 12 "time" 13 14 "github.com/atlassian/git-lob/util" 15 ) 16 17 // Git specification of a commit or range of commits (a reference or reference range) 18 type GitRefSpec struct { 19 // First ref 20 Ref1 string 21 // Optional range operator if this is a range refspec (".." or "...") 22 RangeOp string 23 // Optional second ref 24 Ref2 string 25 } 26 27 // Some top level information about a commit (only first line of message) 28 type GitCommitSummary struct { 29 SHA string 30 ShortSHA string 31 Parents []string 32 CommitDate time.Time 33 AuthorDate time.Time 34 AuthorName string 35 AuthorEmail string 36 CommitterName string 37 CommitterEmail string 38 Subject string 39 } 40 41 type GitRefType int 42 43 const ( 44 GitRefTypeLocalBranch = GitRefType(iota) 45 GitRefTypeRemoteBranch = GitRefType(iota) 46 GitRefTypeLocalTag = GitRefType(iota) 47 GitRefTypeRemoteTag = GitRefType(iota) 48 GitRefTypeHEAD = GitRefType(iota) // current checkout 49 GitRefTypeOther = GitRefType(iota) // stash or unknown 50 ) 51 52 // A git reference (branch, tag etc) 53 type GitRef struct { 54 Name string 55 Type GitRefType 56 CommitSHA string 57 } 58 59 // Returns whether a GitRefSpec is a range or not 60 func (r *GitRefSpec) IsRange() bool { 61 return (r.RangeOp == ".." || r.RangeOp == "...") && 62 r.Ref1 != "" && r.Ref2 != "" 63 } 64 65 // Returns whether a GitRefSpec is an empty range (using the same ref for start & end) 66 func (r *GitRefSpec) IsEmptyRange() bool { 67 return (r.RangeOp == ".." || r.RangeOp == "...") && 68 r.Ref1 != "" && r.Ref1 == r.Ref2 69 } 70 71 func (r *GitRefSpec) String() string { 72 if r.IsRange() { 73 return fmt.Sprintf("%v%v%v", r.Ref1, r.RangeOp, r.Ref2) 74 } else { 75 return r.Ref1 76 } 77 } 78 79 // A record of a set of LOB shas that are associated with a commit 80 type CommitLOBRef struct { 81 Commit string 82 Parents []string 83 // Bare LOBs 84 LobSHAs []string 85 // LOBs with file names 86 FileLOBs []*FileLOB 87 } 88 89 func (self *CommitLOBRef) String() string { 90 return fmt.Sprintf("Commit: %v\n Files:%v\n", self.Commit, self.LobSHAs) 91 } 92 93 // A filename & LOB SHA pair 94 type FileLOB struct { 95 // Filename relative to repository root 96 Filename string 97 // LOB SHA 98 SHA string 99 } 100 101 // Convert a slice of FileLOBs to a map of lob sha to filename, eliminates duplicates 102 func ConvertFileLOBSliceToMap(slice []*FileLOB) map[string]string { 103 ret := make(map[string]string, len(slice)) 104 for _, filelob := range slice { 105 ret[filelob.SHA] = filelob.Filename 106 } 107 return ret 108 } 109 110 // Walk first parents starting from startSHA and call callback 111 // First call will be startSHA & its parent 112 // Parent will be blank string if there are no more parents & walk will stop after 113 // Optimises internally to call Git only for batches of 50 114 func WalkGitHistory(startSHA string, callback func(currentSHA, parentSHA string) (quit bool, err error)) error { 115 116 quit := false 117 currentLogHEAD := startSHA 118 var callbackError error 119 for !quit { 120 // get 250 parents 121 // format as <SHA> <PARENT> so we can detect the end of history 122 cmd := exec.Command("git", "log", "--first-parent", "--topo-order", 123 "-n", "250", "--format=%H %P", currentLogHEAD) 124 125 outp, err := cmd.StdoutPipe() 126 if err != nil { 127 return errors.New(fmt.Sprintf("Unable to list commits from %v: %v", currentLogHEAD, err.Error())) 128 } 129 cmd.Start() 130 scanner := bufio.NewScanner(outp) 131 var currentLine string 132 var parentSHA string 133 for scanner.Scan() { 134 currentLine = scanner.Text() 135 currentSHA := currentLine[:40] 136 // If we got here, we still haven't found an ancestor that was already marked 137 // check next batch, provided there's a parent on the last one 138 // 81 chars long, 2x40 SHAs + space 139 if len(currentLine) >= 81 { 140 parentSHA = strings.TrimSpace(currentLine[41:81]) 141 } else { 142 parentSHA = "" 143 } 144 quit, callbackError = callback(currentSHA, parentSHA) 145 if quit { 146 cmd.Process.Kill() 147 break 148 } 149 } 150 cmd.Wait() 151 // End of history 152 if parentSHA == "" { 153 break 154 } else { 155 currentLogHEAD = parentSHA 156 } 157 } 158 return callbackError 159 } 160 161 // Walk forwards through a list of commits with LOB references based on refspec 162 // If refspec is a range, walks that specific range of commits regardless of whether it's been pushed 163 // If not, walks forwards from the oldest ancestor of refspec.Ref1 that's not pushed to the latest commit (including 'ref' if it includes LOBs) 164 // Walks all ancestors including second+ parents, in topological order 165 // remoteName can be a specific remote or "*" to count pushed ton *any* remote as OK 166 // If recheck=true then existing pushed records are ignored (all commits are walked) 167 func WalkGitCommitLOBsToPushForRefSpec(remoteName string, refspec *GitRefSpec, recheck bool, callback func(commitLOB *CommitLOBRef) (quit bool, err error)) error { 168 if refspec.IsRange() { 169 // Walk a specific range 170 return walkGitCommitsReferencingLOBsInRange(refspec.Ref1, refspec.Ref2, true, false, []string{}, []string{}, callback) 171 172 } else { 173 // Walk everything that hasn't been pushed before Ref1 174 return WalkGitCommitLOBsToPush(remoteName, refspec.Ref1, recheck, callback) 175 } 176 } 177 178 // Walk a list of commits with LOB references which are ancestors of 'ref' which have not been pushed 179 // Walks forwards from the oldest commit to the latest commit (including 'ref' if it includes LOBs) 180 // Walks all ancestors including second+ parents, in topological order 181 // remoteName can be a specific remote or "*" to count pushed ton *any* remote as OK 182 func WalkGitCommitLOBsToPush(remoteName, ref string, recheck bool, callback func(commitLOB *CommitLOBRef) (quit bool, err error)) error { 183 // We use git's ability to log all new commits up to ref but exclude any ancestors of pushed 184 var pushedSHAs []string 185 // If rechecking, then we just log the whole thing 186 if !recheck { 187 pushedSHAs = GetPushedCommits(remoteName) 188 } 189 // Loop to allow retry 190 for { 191 args := []string{"log", `--format=commitsha: %H %P`, "-p", 192 "--topo-order", 193 "--reverse", 194 "-G", SHALineRegexStr, 195 ref} 196 197 for _, p := range pushedSHAs { 198 // 'not reachable from pushed commits' 199 args = append(args, fmt.Sprintf("^%v", p)) 200 } 201 202 // format as <SHA> <PARENT> so we progressively work backward 203 cmd := exec.Command("git", args...) 204 205 outp, err := cmd.StdoutPipe() 206 if err != nil { 207 return errors.New(fmt.Sprintf("Unable to list commits from %v: %v", ref, err.Error())) 208 } 209 cmd.Start() 210 211 quit, err := walkGitLogOutputForLOBReferences(outp, true, false, []string{}, []string{}, callback) 212 213 if quit || err != nil { 214 // Early abort 215 cmd.Process.Kill() 216 } 217 218 procerr := cmd.Wait() 219 if procerr != nil { 220 if len(pushedSHAs) > 0 { 221 // This can happen because one of the pushedSHAs has been completely removed from the repo 222 // consolidate SHAs and try again, this deletes any non-existent SHAs 223 consolidated := consolidateCommitsToLatestDescendants(pushedSHAs) 224 if len(consolidated) != len(pushedSHAs) { 225 // Store the refined state 226 WritePushedState(remoteName, consolidated) 227 pushedSHAs = consolidated 228 // retry 229 continue 230 } 231 } 232 } 233 234 return err 235 236 } 237 } 238 239 // Internal utility for walking git-log output for git-lob references & calling callback 240 // Log output must be formated like this: `--format=commitsha: %H %P` 241 // outp must be output from a running git log task 242 func walkGitLogOutputForLOBReferences(outp io.Reader, additions, removals bool, 243 includePaths, excludePaths []string, callback func(commitLOB *CommitLOBRef) (quit bool, err error)) (quit bool, err error) { 244 // Sadly we still get more output than we actually need, but this is the minimum we can get 245 // For each commit we'll get something like this: 246 /* 247 commitsha: af2607421c9fee2e430cde7e7073a7dad07be559 22be911a626eb9cf2e2760b1b8b092441771cb9d 248 249 diff --git a/atheneNormalMap.png b/atheneNormalMap.png 250 new file mode 100644 251 index 0000000..272b5c1 252 --- /dev/null 253 +++ b/atheneNormalMap.png 254 @@ -0,0 +1 @@ 255 +git-lob: b022770eab414c36575290c993c29799bc6610c3 256 */ 257 // There can be multiple diffs per commit (multiple binaries) 258 // Also when a binary is changed the diff will include a '-' line for the old SHA 259 // Depending on which direction in history the caller wants, they'll specify the 260 // parameters 'additions' and 'removals' to determine which get included 261 262 // Use 1 regex to capture all for speed 263 var lobregex *regexp.Regexp 264 if additions && !removals { 265 lobregex = regexp.MustCompile(`^\+git-lob: ([A-Fa-f0-9]{40})`) 266 } else if removals && !additions { 267 lobregex = regexp.MustCompile(`^\-git-lob: ([A-Fa-f0-9]{40})`) 268 } else { 269 lobregex = regexp.MustCompile(`^[\+\-]git-lob: ([A-Fa-f0-9]{40})`) 270 } 271 fileHeaderRegex := regexp.MustCompile(`diff --git a\/(.+?)\s+b\/(.+)`) 272 fileMergeHeaderRegex := regexp.MustCompile(`diff --cc (.+)`) 273 commitHeaderRegex := regexp.MustCompile(`^commitsha: ([A-Fa-f0-9]{40})(?: ([A-Fa-f0-9]{40}))*`) 274 275 scanner := bufio.NewScanner(outp) 276 277 var currentCommit *CommitLOBRef 278 var currentFilename string 279 currentFileIncluded := true 280 for scanner.Scan() { 281 line := scanner.Text() 282 if match := commitHeaderRegex.FindStringSubmatch(line); match != nil { 283 // Commit header 284 sha := match[1] 285 parentSHAs := match[2:] 286 // Set commit context 287 if currentCommit != nil { 288 if len(currentCommit.LobSHAs) > 0 { 289 quit, err := callback(currentCommit) 290 if err != nil { 291 return quit, err 292 } else if quit { 293 return true, nil 294 } 295 } 296 currentCommit = nil 297 } 298 currentCommit = &CommitLOBRef{Commit: sha, Parents: parentSHAs} 299 } else if match := fileHeaderRegex.FindStringSubmatch(line); match != nil { 300 // Finding a regular file header 301 // Pertinent file name depends on whether we're listening to additions or removals 302 if additions { 303 currentFilename = match[2] 304 } else { 305 currentFilename = match[1] 306 } 307 currentFileIncluded = util.FilenamePassesIncludeExcludeFilter(currentFilename, includePaths, excludePaths) 308 } else if match := fileMergeHeaderRegex.FindStringSubmatch(line); match != nil { 309 // Git merge file header is a little different, only one file 310 currentFilename = match[1] 311 currentFileIncluded = util.FilenamePassesIncludeExcludeFilter(currentFilename, includePaths, excludePaths) 312 } else if match := lobregex.FindStringSubmatch(line); match != nil { 313 // This is a LOB reference (+/- already matched in variant of regex) 314 sha := match[1] 315 // Use filename context to include/exclude if paths were used 316 if currentFileIncluded { 317 currentCommit.LobSHAs = append(currentCommit.LobSHAs, sha) 318 currentCommit.FileLOBs = append(currentCommit.FileLOBs, &FileLOB{Filename: currentFilename, SHA: sha}) 319 } 320 } 321 } 322 // Final commit 323 if currentCommit != nil { 324 if len(currentCommit.LobSHAs) > 0 { 325 quit, err := callback(currentCommit) 326 if err != nil { 327 return quit, err 328 } else if quit { 329 return true, nil 330 } 331 } 332 currentCommit = nil 333 } 334 335 return false, nil 336 } 337 338 // Gets the default push remote for the working dir 339 // Determined from branch.*.remote configuration for the 340 // current branch if present, or defaults to origin. 341 func GetGitDefaultRemoteForPush() string { 342 343 remote, ok := util.GlobalOptions.GitConfig[fmt.Sprintf("branch.%v.remote", GetGitCurrentBranch())] 344 if ok { 345 return remote 346 } 347 return "origin" 348 349 } 350 351 // Gets the default fetch remote for the working dir 352 // Determined from tracking state of current branch 353 // if present, or defaults to origin. 354 func GetGitDefaultRemoteForPull() string { 355 356 remoteName, _ := GetGitUpstreamBranch(GetGitCurrentBranch()) 357 if remoteName != "" { 358 return remoteName 359 } 360 return "origin" 361 } 362 363 // Get a list of git remotes 364 func GetGitRemotes() ([]string, error) { 365 cmd := exec.Command("git", "remote") 366 outp, err := cmd.StdoutPipe() 367 if err != nil { 368 return []string{}, fmt.Errorf("Error calling 'git remote': %v", err.Error()) 369 } 370 scanner := bufio.NewScanner(outp) 371 cmd.Start() 372 var ret []string 373 for scanner.Scan() { 374 ret = append(ret, scanner.Text()) 375 } 376 cmd.Wait() 377 return ret, nil 378 379 } 380 381 func IsGitRemote(remoteName string) bool { 382 remotes, err := GetGitRemotes() 383 if err != nil { 384 return false 385 } 386 sort.Strings(remotes) 387 ret, _ := util.StringBinarySearch(remotes, remoteName) 388 return ret 389 } 390 391 var cachedCurrentBranch string 392 393 // Get the name of the current branch 394 func GetGitCurrentBranch() string { 395 // Use cache, we never switch branches ourselves within lifetime so save some 396 // repeat calls if queried more than once 397 if cachedCurrentBranch == "" { 398 cmd := exec.Command("git", "branch") 399 400 outp, err := cmd.StdoutPipe() 401 if err != nil { 402 util.LogErrorf("Unable to get current branch: %v", err.Error()) 403 return "" 404 } 405 cmd.Start() 406 scanner := bufio.NewScanner(outp) 407 found := false 408 for scanner.Scan() { 409 line := scanner.Text() 410 411 if line[0] == '*' { 412 cachedCurrentBranch = line[2:] 413 found = true 414 break 415 } 416 } 417 cmd.Wait() 418 419 // There's a special case in a newly initialised repository where 'git branch' returns nothing at all 420 // In this case the branch really is 'master' 421 if !found { 422 cachedCurrentBranch = "master" 423 } 424 } 425 426 return cachedCurrentBranch 427 428 } 429 430 // Parse a single git refspec string into a GitRefSpec structure ie identify ranges if present 431 // Does not perform any validation since refs can be symbolic anyway, up to the caller 432 // to check whether the returned refspec actually works 433 func ParseGitRefSpec(s string) *GitRefSpec { 434 435 if idx := strings.Index(s, "..."); idx != -1 { 436 // reachable from ref1 OR ref2, not both 437 ref1 := strings.TrimSpace(s[:idx]) 438 ref2 := strings.TrimSpace(s[idx+3:]) 439 return &GitRefSpec{ref1, "...", ref2} 440 } else if idx := strings.Index(s, ".."); idx != -1 { 441 // range from ref1 -> ref2 442 ref1 := strings.TrimSpace(s[:idx]) 443 ref2 := strings.TrimSpace(s[idx+2:]) 444 return &GitRefSpec{ref1, "..", ref2} 445 } else { 446 ref1 := strings.TrimSpace(s) 447 return &GitRefSpec{Ref1: ref1} 448 } 449 450 } 451 452 var IsSHARegex *regexp.Regexp = regexp.MustCompile("^[0-9A-Fa-f]{8,40}$") 453 454 // Return whether a single git reference (not refspec, so no ranges) is a full SHA or not 455 // SHAs can be used directly for things like lob lookup but other refs have too be converted 456 // This version requires a full length SHA (40 characters) 457 func GitRefIsFullSHA(ref string) bool { 458 return len(ref) == 40 && IsSHARegex.MatchString(ref) 459 } 460 461 // Return whether a single git reference (not refspec, so no ranges) is a SHA or not 462 // SHAs can be used directly for things like lob lookup but other refs have too be converted 463 // This version accepts SHAs that are 8-40 characters in length, so accepts short SHAs 464 func GitRefIsSHA(ref string) bool { 465 return IsSHARegex.MatchString(ref) 466 } 467 468 func GitRefToFullSHA(ref string) (string, error) { 469 if GitRefIsFullSHA(ref) { 470 return ref, nil 471 } 472 // Otherwise use Git to expand to full 40 character SHA 473 cmd := exec.Command("git", "rev-parse", ref) 474 outp, err := cmd.Output() 475 if err != nil { 476 return ref, fmt.Errorf("Unknown or ambiguous ref %v", ref) 477 } 478 return strings.TrimSpace(string(outp)), nil 479 } 480 481 // Returns whether a ref or SHA refers to a valid, existing commit or not by asking git to resolve it 482 func GitRefOrSHAIsValid(refOrSHA string) bool { 483 // --verify doesn't actually verify commit object is valid, will return OK if it's just any 40-char SHA 484 // Need to use <sha>^{commit} to verify it's a commit 485 err := exec.Command("git", "rev-parse", "--verify", 486 fmt.Sprintf("%v^{commit}", refOrSHA)).Run() 487 return err == nil 488 } 489 490 // Return a list of all local branches 491 // Also FYI caches the current branch while we're at it so it's zero-cost to call 492 // GetGitCurrentBranch after this 493 func GetGitLocalBranches() ([]string, error) { 494 cmd := exec.Command("git", "branch") 495 496 outp, err := cmd.StdoutPipe() 497 if err != nil { 498 return []string{}, errors.New(fmt.Sprintf("Unable to get list local branches: %v", err.Error())) 499 } 500 cmd.Start() 501 scanner := bufio.NewScanner(outp) 502 foundcurrent := cachedCurrentBranch != "" 503 var ret []string 504 for scanner.Scan() { 505 line := scanner.Text() 506 if len(line) > 2 { 507 branch := line[2:] 508 ret = append(ret, branch) 509 // While we're at it, cache current branch 510 if !foundcurrent && line[0] == '*' { 511 cachedCurrentBranch = branch 512 foundcurrent = true 513 } 514 515 } 516 517 } 518 cmd.Wait() 519 520 return ret, nil 521 522 } 523 524 // Return a list of all remote branches for a given remote 525 // Note this doesn't retrieve mappings between local and remote branches, just a simple list 526 func GetGitRemoteBranches(remoteName string) ([]string, error) { 527 cmd := exec.Command("git", "branch", "-r") 528 529 outp, err := cmd.StdoutPipe() 530 if err != nil { 531 return []string{}, errors.New(fmt.Sprintf("Unable to get list remote branches: %v", err.Error())) 532 } 533 cmd.Start() 534 scanner := bufio.NewScanner(outp) 535 var ret []string 536 prefix := remoteName + "/" 537 for scanner.Scan() { 538 line := scanner.Text() 539 if len(line) > 2 { 540 line := line[2:] 541 if strings.HasPrefix(line, prefix) { 542 // Make sure we terminate at space, line may include alias 543 remotebranch := strings.Fields(line[len(prefix):])[0] 544 if remotebranch != "HEAD" { 545 ret = append(ret, remotebranch) 546 } 547 } 548 } 549 550 } 551 cmd.Wait() 552 553 return ret, nil 554 555 } 556 557 // Return a list of branches to push by default, based on push.default and local/remote branches 558 // See push.default docs at https://www.kernel.org/pub/software/scm/git/docs/git-config.html 559 func GetGitPushDefaultBranches(remoteName string) []string { 560 pushdef := util.GlobalOptions.GitConfig["push.default"] 561 if pushdef == "" { 562 // Use the git 2.0 'simple' default 563 pushdef = "simple" 564 } 565 566 if pushdef == "matching" { 567 // Multiple branches, but only where remote branch name matches 568 localbranches, err := GetGitLocalBranches() 569 if err != nil { 570 // will be logged, safe return 571 return []string{} 572 } 573 remotebranches, err := GetGitRemoteBranches(remoteName) 574 if err != nil { 575 // will be logged, safe return 576 return []string{} 577 } 578 // Probably sorted already but to be sure 579 sort.Strings(remotebranches) 580 var ret []string 581 for _, branch := range localbranches { 582 present, _ := util.StringBinarySearch(remotebranches, branch) 583 584 if present { 585 ret = append(ret, branch) 586 } 587 } 588 return ret 589 } else if pushdef == "current" || pushdef == "upstream" || pushdef == "simple" { 590 // Current, upstream, simple (in ascending complexity) 591 currentBranch := GetGitCurrentBranch() 592 if pushdef == "current" { 593 return []string{currentBranch} 594 } 595 // For upstream & simple we need to know what the upstream branch is 596 upstreamRemote, upstreamBranch := GetGitUpstreamBranch(currentBranch) 597 // Only proceed if the upstream is on this remote 598 if upstreamRemote == remoteName && upstreamBranch != "" { 599 if pushdef == "upstream" { 600 // For upstream we don't care what the remote branch is called 601 return []string{currentBranch} 602 } else { 603 // "simple" 604 // In this case git would only push if remote branch matches as well 605 if upstreamBranch == currentBranch { 606 return []string{currentBranch} 607 } 608 } 609 } 610 } 611 612 // "nothing", something we don't understand (safety), or fallthrough non-matched 613 return []string{} 614 615 } 616 617 // Get the upstream branch for a given local branch, as defined in what 'git pull' would do by default 618 // returns the remote name and the remote branch separately for ease of use 619 func GetGitUpstreamBranch(localbranch string) (remoteName, remoteBranch string) { 620 // Super-verbose mode gives us tracking branch info 621 cmd := exec.Command("git", "branch", "-vv") 622 623 outp, err := cmd.StdoutPipe() 624 if err != nil { 625 util.LogErrorf("Unable to get list branches: %v", err.Error()) 626 return "", "" 627 } 628 cmd.Start() 629 scanner := bufio.NewScanner(outp) 630 631 // Output is like this: 632 // branch1 387def9 [origin/branch1] Another new branch 633 // * master aec3297 [origin/master: behind 1] Master change 634 // * feature1 e88c156 [origin/feature1: ahead 4, behind 6] Something something dark side 635 // nottrackingbranch f33e451 Some message 636 637 // Extract branch name and tracking branch (won't match branches with no tracking) 638 // Stops at ']' or ':' in tracking branch to deal with ahead/behind markers 639 trackRegex := regexp.MustCompile(`^[* ] (\S+)\s+[a-fA-F0-9]+\s+\[([^/]+)/([^\:]+)[\]:]`) 640 641 for scanner.Scan() { 642 line := scanner.Text() 643 if match := trackRegex.FindStringSubmatch(line); match != nil { 644 lbranch := match[1] 645 if lbranch == localbranch { 646 return match[2], match[3] 647 } 648 } 649 650 } 651 cmd.Wait() 652 653 // no tracking for this branch 654 return "", "" 655 656 } 657 658 // Returns list of commits which have LOB SHAs referenced in them, in a given commit range 659 // Commits will be in ASCENDING order (parents before children) unlike WalkGitHistory 660 // Either of from, to or both can be blank to have an unbounded range of commits based on current HEAD 661 // It is required that if both are supplied, 'from' is an ancestor of 'to' 662 // Range is exclusive of 'from' and inclusive of 'to' 663 func GetGitCommitsReferencingLOBsInRange(from, to string, includePaths, excludePaths []string) ([]*CommitLOBRef, error) { 664 // We want '+' lines 665 return getGitCommitsReferencingLOBsInRange(from, to, true, false, includePaths, excludePaths) 666 } 667 668 // Returns list of commits which have LOB SHAs referenced in them, in a given commit range 669 // Range is exclusive of 'from' and inclusive of 'to' 670 // additions/removals controls whether we report only diffs with '+' lines of git-lob, '-' lines, or both 671 func getGitCommitsReferencingLOBsInRange(from, to string, additions, removals bool, includePaths, excludePaths []string) ([]*CommitLOBRef, error) { 672 var ret []*CommitLOBRef 673 callback := func(commit *CommitLOBRef) (quit bool, err error) { 674 ret = append(ret, commit) 675 return false, nil 676 } 677 err := walkGitCommitsReferencingLOBsInRange(from, to, additions, removals, includePaths, excludePaths, callback) 678 return ret, err 679 } 680 681 // Walks a list of commits in ascending order which have LOB SHAs referenced in them, in a given commit range 682 // Range is exclusive of 'from' and inclusive of 'to' 683 // additions/removals controls whether we report only diffs with '+' lines of git-lob, '-' lines, or both 684 func walkGitCommitsReferencingLOBsInRange(from, to string, additions, removals bool, includePaths, excludePaths []string, 685 callback func(commit *CommitLOBRef) (quit bool, err error)) error { 686 687 args := []string{"log", `--format=commitsha: %H %P`, "-p", 688 "--topo-order", "--first-parent", 689 "--reverse", // we want to list them in ascending order 690 "-G", SHALineRegexStr} 691 692 if from != "" && to != "" { 693 args = append(args, fmt.Sprintf("%v..%v", from, to)) 694 } else { 695 if to != "" { 696 args = append(args, to) 697 } else if from != "" { 698 args = append(args, fmt.Sprintf("%v..HEAD", from)) 699 } 700 // if from & to are both blank, just use default behaviour of git log 701 } 702 703 cmd := exec.Command("git", args...) 704 outp, err := cmd.StdoutPipe() 705 if err != nil { 706 return errors.New(fmt.Sprintf("Unable to call git-log: %v", err.Error())) 707 } 708 cmd.Start() 709 710 _, err = walkGitLogOutputForLOBReferences(outp, additions, removals, includePaths, excludePaths, callback) 711 712 cmd.Wait() 713 714 return err 715 716 } 717 718 // Gets a list of LOB SHAs for all binary files that are needed when checking out any of 719 // the commits referred to by refspec. 720 // As opposed to GetGitCommitsReferencingLOBsInRange which only picks up changes to LOBs, 721 // this function returns the complete set of LOBs needed if you checked out a commit either at 722 // a single commit, or any in a range (if the refspec is a range; only .. range operator allowed) 723 // This means it will include any LOBs that were added in commits before the range, if they are still used, 724 // while GetGitCommitsReferencingLOBsInRange wouldn't mention those. 725 // Note that git ranges are start AND end inclusive in this case. 726 // Note that duplicate SHAs are not eliminated for efficiency, you must do it if you need it 727 func GetGitAllLOBsToCheckoutInRefSpec(refspec *GitRefSpec, includePaths, excludePaths []string) ([]string, error) { 728 729 var snapshotref string 730 if refspec.IsRange() { 731 if refspec.RangeOp != ".." { 732 return []string{}, errors.New("Only '..' range operator allowed in GetGitAllLOBsToCheckoutInRefSpec") 733 } 734 // snapshot at end of range, then look at diffs later 735 snapshotref = refspec.Ref2 736 } else { 737 snapshotref = refspec.Ref1 738 } 739 740 ret, err := GetGitAllLOBsToCheckoutAtCommit(snapshotref, includePaths, excludePaths) 741 if err != nil { 742 return ret, err 743 } 744 745 if refspec.IsRange() { 746 // Now we have all LOBs at the snapshot, find any extra ones earlier in the range 747 // to do this, we look for diffs in the commit range that start with "-git-lob:" 748 // because a removal means it was referenced before that commit therefore we need it 749 // to go back to that state 750 // git log is range start exclusive, but that's actually OK since a -git-lob diff line 751 // represents the state one commit earlier, giving us an inclusive start range 752 commits, err := getGitCommitsReferencingLOBsInRange(refspec.Ref1, refspec.Ref2, false, true, includePaths, excludePaths) 753 if err != nil { 754 return ret, err 755 } 756 for _, commit := range commits { 757 // possible to end up with duplicates here if same SHA referenced more than once 758 // caller to resolve if they need uniques 759 ret = append(ret, commit.LobSHAs...) 760 } 761 762 } 763 764 return ret, nil 765 766 } 767 768 // Gets a list of LOB SHAs with their filenames for all binary files that are needed when checking out any of 769 // the commits referred to by refspec. 770 // As opposed to GetGitCommitsReferencingLOBsInRange which only picks up changes to LOBs, 771 // this function returns the complete set of LOBs needed if you checked out a commit either at 772 // a single commit, or any in a range (if the refspec is a range; only .. range operator allowed) 773 // This means it will include any LOBs that were added in commits before the range, if they are still used, 774 // while GetGitCommitsReferencingLOBsInRange wouldn't mention those. 775 // Note that git ranges are start AND end inclusive in this case. 776 // Note that duplicate SHAs are not eliminated for efficiency, you must do it if you need it 777 func GetGitAllFilesAndLOBsToCheckoutInRefSpec(refspec *GitRefSpec, includePaths, excludePaths []string) ([]*FileLOB, error) { 778 779 var snapshotref string 780 if refspec.IsRange() { 781 if refspec.RangeOp != ".." { 782 return nil, errors.New("Only '..' range operator allowed in GetGitAllLOBsToCheckoutInRefSpec") 783 } 784 // snapshot at end of range, then look at diffs later 785 snapshotref = refspec.Ref2 786 } else { 787 snapshotref = refspec.Ref1 788 } 789 790 ret, err := GetGitAllFilesAndLOBsToCheckoutAtCommit(snapshotref, includePaths, excludePaths) 791 if err != nil { 792 return ret, err 793 } 794 795 if refspec.IsRange() { 796 // Now we have all LOBs at the snapshot, find any extra ones earlier in the range 797 // to do this, we look for diffs in the commit range that start with "-git-lob:" 798 // because a removal means it was referenced before that commit therefore we need it 799 // to go back to that state 800 // git log is range start exclusive, but that's actually OK since a -git-lob diff line 801 // represents the state one commit earlier, giving us an inclusive start range 802 commits, err := getGitCommitsReferencingLOBsInRange(refspec.Ref1, refspec.Ref2, false, true, includePaths, excludePaths) 803 if err != nil { 804 return ret, err 805 } 806 for _, commit := range commits { 807 // possible to end up with duplicates here if same SHA referenced more than once 808 // caller to resolve if they need uniques 809 ret = append(ret, commit.FileLOBs...) 810 } 811 812 } 813 814 return ret, nil 815 816 } 817 818 // Get all the LOB SHAs that you would need to have available to check out a commit, and any other 819 // ancestor of it within a number of days of that commit date (not today's date) 820 // Note that if a LOB was modified to the same SHA more than once, duplicates may appear in the return 821 // They are not routinely eliminated for performance, so perform your own dupe removal if you need it 822 // as well as a list of LOBs, returns the commit SHA of the earliest change that was included in the scan. 823 // Since this is the first *change* included (which would be removing the previous SHA), the earliest LOB 824 // SHA included is from the *parent* of this commit. 825 func GetGitAllLOBsToCheckoutAtCommitAndRecent(commit string, days int, includePaths, 826 excludePaths []string) (lobs []string, earliestChangeCommit string, reterr error) { 827 // All LOBs at the commit itself 828 shasAtCommit, err := GetGitAllLOBsToCheckoutAtCommit(commit, includePaths, excludePaths) 829 if err != nil { 830 return nil, "", err 831 } 832 833 // days == 0 means we only snapshot latest 834 if days == 0 { 835 earliest := commit 836 if !GitRefIsFullSHA(earliest) { 837 earliest, _ = GitRefToFullSHA(earliest) 838 } 839 return shasAtCommit, earliest, nil 840 } else { 841 ret := shasAtCommit 842 earliestCommit := commit 843 callback := func(lobcommit *CommitLOBRef) (quit bool, err error) { 844 ret = append(ret, lobcommit.LobSHAs...) 845 earliestCommit = lobcommit.Commit 846 return false, nil 847 } 848 err := walkGitAllLOBsInRecentCommits(commit, days, includePaths, excludePaths, callback) 849 850 return ret, earliestCommit, err 851 } 852 853 } 854 855 // Get all the Filenames & LOB SHAs that you would need to have available to check out a commit, and any other 856 // ancestor of it within a number of days of that commit date (not today's date) 857 // Note that if a LOB was modified to the same SHA more than once, duplicates may appear in the return 858 // They are not routinely eliminated for performance, so perform your own dupe removal if you need it 859 // as well as a list of LOBs, returns the commit SHA of the earliest change that was included in the scan. 860 // Since this is the first *change* included (which would be removing the previous SHA), the earliest LOB 861 // SHA included is from the *parent* of this commit. 862 func GetGitAllFileLOBsToCheckoutAtCommitAndRecent(commit string, days int, includePaths, 863 excludePaths []string) (filelobs []*FileLOB, earliestChangeCommit string, reterr error) { 864 // All LOBs at the commit itself 865 fileshasAtCommit, err := GetGitAllFilesAndLOBsToCheckoutAtCommit(commit, includePaths, excludePaths) 866 if err != nil { 867 return nil, "", err 868 } 869 870 // days == 0 means we only snapshot latest 871 if days == 0 { 872 earliest := commit 873 if !GitRefIsFullSHA(earliest) { 874 earliest, _ = GitRefToFullSHA(earliest) 875 } 876 return fileshasAtCommit, earliest, nil 877 } else { 878 ret := fileshasAtCommit 879 earliestCommit := commit 880 callback := func(lobcommit *CommitLOBRef) (quit bool, err error) { 881 ret = append(ret, lobcommit.FileLOBs...) 882 earliestCommit = lobcommit.Commit 883 return false, nil 884 } 885 err := walkGitAllLOBsInRecentCommits(commit, days, includePaths, excludePaths, callback) 886 887 return ret, earliestCommit, err 888 } 889 890 } 891 892 // Walk backwards in history looking for all ancestors and references to LOBs in the '-' side of the diff 893 func walkGitAllLOBsInRecentCommits(startcommit string, days int, includePaths, excludePaths []string, 894 callback func(lobcommit *CommitLOBRef) (quit bool, err error)) error { 895 // get the commit date 896 commitDetails, err := GetGitCommitSummary(startcommit) 897 if err != nil { 898 return err 899 } 900 sinceDate := commitDetails.CommitDate.AddDate(0, 0, -days) 901 // Now use git log to scan backwards 902 // We use git log from commit backwards, not commit^ (parent) because 903 // we're looking for *previous* SHAs, which means we're looking for diffs 904 // with a '-' line. So SHAs replaced in the latest commit are old versions too 905 // that we haven't included yet in fileshasAtCommit 906 args := []string{"log", `--format=commitsha: %H %P`, "-p", 907 fmt.Sprintf("--since=%v", FormatGitDate(sinceDate)), 908 "-G", SHALineRegexStr, 909 startcommit} 910 911 cmd := exec.Command("git", args...) 912 outp, err := cmd.StdoutPipe() 913 if err != nil { 914 return errors.New(fmt.Sprintf("Unable to call git-log: %v", err.Error())) 915 } 916 cmd.Start() 917 918 // Looking backwards, so removals 919 walkGitLogOutputForLOBReferences(outp, false, true, includePaths, excludePaths, callback) 920 921 cmd.Wait() 922 923 return nil 924 } 925 926 // Return a slice of LOB SHAs representing versions of filename, ordered by latest first 927 // history is from all heads not just checked out 928 // if shatoskip is supplied, this sha is excluded from the return if found 929 func GetGitAllLOBHistoryForFile(filename, shatoskip string) ([]string, error) { 930 931 // Scan ALL history for this filename that includes a git-lob marker 932 // not just history from checked out 933 args := []string{"log", `--format=commitsha: %H %P`, "-p", 934 "--all", "--topo-order", // ALL history in reverse order 935 "-G", SHALineRegexStr, 936 "--", filename} 937 938 cmd := exec.Command("git", args...) 939 outp, err := cmd.StdoutPipe() 940 if err != nil { 941 return nil, errors.New(fmt.Sprintf("Unable to call git-log: %v", err.Error())) 942 } 943 cmd.Start() 944 945 // We'll just look for additions ever, walking backwards 946 var ret []string 947 callback := func(commitLOB *CommitLOBRef) (quit bool, err error) { 948 // Already filtered by filename so there can only be one entry, but be sure 949 if len(commitLOB.FileLOBs) == 1 { 950 sha := commitLOB.FileLOBs[0].SHA 951 if sha != shatoskip { 952 ret = append(ret, sha) 953 } 954 } 955 return false, nil 956 } 957 walkGitLogOutputForLOBReferences(outp, true, false, nil, nil, callback) 958 959 cmd.Wait() 960 961 return ret, nil 962 963 } 964 965 // Get all the binary files & their LOB SHAs that you would need to check out at a given commit (not changed in that commit) 966 func GetGitAllFilesAndLOBsToCheckoutAtCommit(commit string, includePaths, excludePaths []string) ([]*FileLOB, error) { 967 var ret []*FileLOB 968 err := WalkGitAllLOBsToCheckoutAtCommit(commit, includePaths, excludePaths, func(filelob *FileLOB) { 969 ret = append(ret, filelob) 970 }) 971 return ret, err 972 } 973 974 // Get all the LOB SHAs that you would need to check out at a given commit (not changed in that commit) 975 func GetGitAllLOBsToCheckoutAtCommit(commit string, includePaths, excludePaths []string) ([]string, error) { 976 var ret []string 977 err := WalkGitAllLOBsToCheckoutAtCommit(commit, includePaths, excludePaths, func(filelob *FileLOB) { 978 ret = append(ret, filelob.SHA) 979 }) 980 return ret, err 981 } 982 983 // Utility function to walk through all the LOBs which are present if checked out at a specific commit 984 func WalkGitAllLOBsToCheckoutAtCommit(commit string, includePaths, excludePaths []string, 985 callback func(filelob *FileLOB)) error { 986 987 // Snapshot using ls-tree 988 args := []string{"ls-tree", 989 "-r", // recurse 990 "-l", // report object size (we'll need this) 991 "--full-tree", // start at the root regardless of where we are in it 992 commit} 993 994 lstreecmd := exec.Command("git", args...) 995 outp, err := lstreecmd.StdoutPipe() 996 if err != nil { 997 return errors.New(fmt.Sprintf("Unable to call git ls-tree: %v", err.Error())) 998 } 999 defer outp.Close() 1000 lstreecmd.Start() 1001 lstreescanner := bufio.NewScanner(outp) 1002 1003 // We will look for objects that are *exactly* the size of the git-lob line 1004 regex := regexp.MustCompile(fmt.Sprintf(`^\d+\s+blob\s+([0-9a-zA-Z]{40})\s+%d\s+(.*)$`, SHALineLen)) 1005 // This will give us object SHAs of content which is exactly the right size, we must 1006 // then use cat-file (in batch mode) to get the content & parse out anything that's really 1007 // a git-lob reference. 1008 // Start git cat-file in parallel and feed its stdin 1009 catfilecmd := exec.Command("git", "cat-file", "--batch") 1010 catout, err := catfilecmd.StdoutPipe() 1011 if err != nil { 1012 return errors.New(fmt.Sprintf("Unable to call git cat-file: %v", err.Error())) 1013 } 1014 defer catout.Close() 1015 catin, err := catfilecmd.StdinPipe() 1016 if err != nil { 1017 return errors.New(fmt.Sprintf("Unable to call git cat-file: %v", err.Error())) 1018 } 1019 defer catin.Close() 1020 catfilecmd.Start() 1021 catscanner := bufio.NewScanner(catout) 1022 1023 for lstreescanner.Scan() { 1024 line := lstreescanner.Text() 1025 if match := regex.FindStringSubmatch(line); match != nil { 1026 objsha := match[1] 1027 filename := match[2] 1028 // Apply filter 1029 if !util.FilenamePassesIncludeExcludeFilter(filename, includePaths, excludePaths) { 1030 continue 1031 } 1032 // Now feed object sha to cat-file to get git-lob SHA if any 1033 // remember we're already only finding files of exactly the right size (49 bytes) 1034 _, err := catin.Write([]byte(objsha)) 1035 if err != nil { 1036 return errors.New(fmt.Sprintf("Unable to write to cat-file stream: %v", err.Error())) 1037 } 1038 _, err = catin.Write([]byte{'\n'}) 1039 if err != nil { 1040 return errors.New(fmt.Sprintf("Unable to write to cat-file stream: %v", err.Error())) 1041 } 1042 1043 // Now read back response - first line is report of object sha, type & size 1044 // second line is content in our case 1045 if !catscanner.Scan() || !catscanner.Scan() { 1046 return errors.New(fmt.Sprintf("Couldn't read response from cat-file stream: %v", catscanner.Err())) 1047 } 1048 1049 // object SHA is the last 40 characters, after the prefix 1050 line := catscanner.Text() 1051 if len(line) == SHALineLen { 1052 lobsha := line[len(SHAPrefix):] 1053 // call callback to process result 1054 callback(&FileLOB{filename, lobsha}) 1055 } 1056 1057 } 1058 } 1059 lstreecmd.Wait() 1060 catfilecmd.Process.Kill() 1061 1062 return nil 1063 1064 } 1065 1066 // Parse a Git date formatted in ISO 8601 format (%ci/%ai) 1067 func ParseGitDate(str string) (time.Time, error) { 1068 1069 // Unfortunately Go and Git don't overlap in their builtin date formats 1070 // Go's time.RFC1123Z and Git's %cD are ALMOST the same, except that 1071 // when the day is < 10 Git outputs a single digit, but Go expects a leading 1072 // zero - this is enough to break the parsing. Sigh. 1073 1074 // Format is for 2 Jan 2006, 15:04:05 -7 UTC as per Go 1075 return time.Parse("2006-01-02 15:04:05 -0700", str) 1076 } 1077 1078 // Format a date into Git format 1079 func FormatGitDate(t time.Time) string { 1080 // Git format is "Fri Jun 21 20:26:41 2013 +0900" but no zero-leading for day 1081 return t.Format("Mon Jan 2 15:04:05 2006 -0700") 1082 } 1083 1084 // Get summary information about a commit 1085 func GetGitCommitSummary(commit string) (*GitCommitSummary, error) { 1086 cmd := exec.Command("git", "show", "-s", 1087 `--format=%H|%h|%P|%ai|%ci|%ae|%an|%ce|%cn|%s`, commit) 1088 1089 out, err := cmd.CombinedOutput() 1090 if err != nil { 1091 msg := fmt.Sprintf("Error calling git show: %v", err.Error()) 1092 return nil, errors.New(msg) 1093 } 1094 1095 // At most 10 substrings so subject line is not split on anything 1096 fields := strings.SplitN(string(out), "|", 10) 1097 // Cope with the case where subject is blank 1098 if len(fields) >= 9 { 1099 ret := &GitCommitSummary{} 1100 // Get SHAs from output, not commit input, so we can support symbolic refs 1101 ret.SHA = fields[0] 1102 ret.ShortSHA = fields[1] 1103 ret.Parents = strings.Split(fields[2], " ") 1104 // %aD & %cD (RFC2822) matches Go's RFC1123Z format 1105 ret.AuthorDate, _ = ParseGitDate(fields[3]) 1106 ret.CommitDate, _ = ParseGitDate(fields[4]) 1107 ret.AuthorEmail = fields[5] 1108 ret.AuthorName = fields[6] 1109 ret.CommitterEmail = fields[7] 1110 ret.CommitterName = fields[8] 1111 if len(fields) > 9 { 1112 ret.Subject = strings.TrimRight(fields[9], "\n") 1113 } 1114 return ret, nil 1115 } else { 1116 msg := fmt.Sprintf("Unexpected output from git show: %v", out) 1117 return nil, errors.New(msg) 1118 } 1119 1120 } 1121 1122 // Get a list of refs (branches, tags) that have received commits in the last numdays, ordered 1123 // by most recent first 1124 // You can also set numdays to -1 to not have any limit but still get them in reverse order 1125 // remoteName is optional but if specified and includeRemoteBranches is true, will only include 1126 // remote branches on that remote 1127 func GetGitRecentRefs(numdays int, includeRemoteBranches bool, remoteName string) ([]*GitRef, error) { 1128 // Include %(objectname) AND %(*objectname), the latter only returns something if it's a tag 1129 // and that will be the dereferenced SHA ie the actual commit SHA instead of the tag SHA 1130 cmd := exec.Command("git", "for-each-ref", 1131 `--sort=-committerdate`, 1132 `--format=%(refname) %(objectname) %(*objectname)`, 1133 "refs") 1134 outp, err := cmd.StdoutPipe() 1135 if err != nil { 1136 msg := fmt.Sprintf("Unable to call git for-each-ref: %v", err.Error()) 1137 return []*GitRef{}, errors.New(msg) 1138 } 1139 cmd.Start() 1140 scanner := bufio.NewScanner(outp) 1141 1142 // Output is like this: 1143 // refs/heads/master 69d144416abf89b79f6a6fd21c2621dd9c13ead1 1144 // refs/remotes/origin/master ad3b29b773e46ad6870fdf08796c33d97190fe93 1145 // refs/tags/blah fa392f757dddf9fa7c3bb1717d0bf0c4762326fc c34b29b773e46ad6870fdf08796c33d97190fe93 1146 // note the second SHA when it's a tag but not otherwise 1147 1148 // Output is ordered by latest commit date first, so we can stop at the threshold 1149 var earliestDate time.Time 1150 if numdays >= 0 { 1151 earliestDate = time.Now().AddDate(0, 0, -numdays) 1152 } 1153 1154 regex := regexp.MustCompile(`^(refs/[^/]+/\S+)\s+([0-9A-Za-z]{40})(?:\s+([0-9A-Za-z]{40}))?`) 1155 1156 var ret []*GitRef 1157 for scanner.Scan() { 1158 line := scanner.Text() 1159 if match := regex.FindStringSubmatch(line); match != nil { 1160 fullref := match[1] 1161 sha := match[2] 1162 // test for dereferenced tags, use commit SHA 1163 if len(match) > 3 && match[3] != "" { 1164 sha = match[3] 1165 } 1166 reftype, ref := ParseGitRefToTypeAndName(fullref) 1167 if reftype == GitRefTypeRemoteBranch || reftype == GitRefTypeRemoteTag { 1168 if !includeRemoteBranches { 1169 continue 1170 } 1171 if remoteName != "" && !strings.HasPrefix(ref, remoteName+"/") { 1172 continue 1173 } 1174 } 1175 // This is a ref we might use 1176 if numdays >= 0 { 1177 // Check the date 1178 commit, err := GetGitCommitSummary(ref) 1179 if err != nil { 1180 return ret, err 1181 } 1182 if commit.CommitDate.Before(earliestDate) { 1183 // the end 1184 break 1185 } 1186 } 1187 ret = append(ret, &GitRef{ref, reftype, sha}) 1188 } 1189 } 1190 cmd.Wait() 1191 1192 return ret, nil 1193 } 1194 1195 // Tell the index to refresh for files which we've modified outside of git commands 1196 // This is necessary because git caches stat() info to provide a fast way to detect 1197 // modifications for git-status and so can consider files modified when they're actually not 1198 // when we've changed things that the filter would consider unmodified when called via git-diff. 1199 // 'files' is a list of files with paths relative to the repo root 1200 func GitRefreshIndexForFiles(files []string) error { 1201 var retErr error 1202 // Since we don't know how many there will be, potentially split into many commands 1203 errorFunc := func(args []string, output string, err error) (abort bool) { 1204 // exit status 1 is not important, it's just '<filename> needs update' 1205 if !strings.HasSuffix(err.Error(), "exit status 1") { 1206 // We actually continue anyway to make sure we try to update all files 1207 // but note this one because it's odd 1208 if retErr == nil { 1209 retErr = fmt.Errorf("Post-checkout index refresh failed: %v", err.Error()) 1210 } else { 1211 retErr = fmt.Errorf("%v\n%v", retErr.Error(), err.Error()) 1212 } 1213 } 1214 return false // don't abort 1215 } 1216 // Need to make file list (which files are relative to repo root) relative to cwd for git's purposes 1217 relfiles := util.MakeRepoFileListRelativeToCwd(files) 1218 util.ExecForManyFilesSplitIfRequired(relfiles, errorFunc, 1219 "git", "update-index", "-q", "--really-refresh", "--") 1220 1221 return retErr 1222 1223 } 1224 1225 // Get the type & name of a git reference 1226 func ParseGitRefToTypeAndName(fullref string) (t GitRefType, name string) { 1227 const localPrefix = "refs/heads/" 1228 const remotePrefix = "refs/remotes/" 1229 const remoteTagPrefix = "refs/remotes/tags/" 1230 const localTagPrefix = "refs/tags/" 1231 1232 if fullref == "HEAD" { 1233 name = fullref 1234 t = GitRefTypeHEAD 1235 } else if strings.HasPrefix(fullref, localPrefix) { 1236 name = fullref[len(localPrefix):] 1237 t = GitRefTypeLocalBranch 1238 } else if strings.HasPrefix(fullref, remotePrefix) { 1239 name = fullref[len(remotePrefix):] 1240 t = GitRefTypeRemoteBranch 1241 } else if strings.HasPrefix(fullref, remoteTagPrefix) { 1242 name = fullref[len(remoteTagPrefix):] 1243 t = GitRefTypeRemoteTag 1244 } else if strings.HasPrefix(fullref, localTagPrefix) { 1245 name = fullref[len(localTagPrefix):] 1246 t = GitRefTypeLocalTag 1247 } else { 1248 name = fullref 1249 t = GitRefTypeOther 1250 } 1251 return 1252 } 1253 1254 // get all refs in the repo (branches, tags, stashes) 1255 func GetGitAllRefs() ([]*GitRef, error) { 1256 cmd := exec.Command("git", "show-ref", "--head", "--dereference") 1257 outp, err := cmd.StdoutPipe() 1258 if err != nil { 1259 return []*GitRef{}, fmt.Errorf("Failure in git-show-ref: %v", err.Error()) 1260 } 1261 scanner := bufio.NewScanner(outp) 1262 var ret []*GitRef 1263 cmd.Start() 1264 1265 // Output is like this: 1266 // <sha> HEAD 1267 // <sha> refs/heads/<branch> 1268 // <sha> refs/tags/<tag> 1269 // <sha> refs/tags/<tag>^{} <- dereferenced tag, should use this one instead of original 1270 // <sha> refs/remotes/<remotebranch> 1271 // <sha> refs/stash (skipped) 1272 1273 for scanner.Scan() { 1274 line := scanner.Text() 1275 1276 f := strings.Fields(line) 1277 if len(f) == 2 { 1278 sha := f[0] 1279 fullref := f[1] 1280 t, name := ParseGitRefToTypeAndName(fullref) 1281 if t == GitRefTypeOther { 1282 // skip all others (including Stash) 1283 continue 1284 } 1285 1286 // Special case dereferenced tags. Non-lightweight tags refer to the tag 1287 // object, not the commit, but --dereference shows you the actual commit 1288 // with an extra ref after the tag object, called <tagname>^{} 1289 // This must take precedence to report the commit it applies to 1290 if t == GitRefTypeLocalTag && strings.HasSuffix(name, "^{}") { 1291 name = name[:len(name)-3] 1292 // now overwrite the previous tag object entry (they always come before) 1293 for _, ref := range ret { 1294 if ref.Name == name { 1295 ref.CommitSHA = sha 1296 } 1297 } 1298 } else { 1299 // Otherwise, new ref 1300 ret = append(ret, &GitRef{Name: name, Type: t, CommitSHA: sha}) 1301 } 1302 1303 } 1304 1305 } 1306 cmd.Wait() 1307 1308 return ret, nil 1309 } 1310 1311 // Returns whether commit a (sha or ref) is an ancestor of commit b (sha or ref) 1312 func GitIsAncestor(a, b string) (bool, error) { 1313 1314 if !GitRefIsSHA(a) { 1315 var err error 1316 a, err = GitRefToFullSHA(a) 1317 if err != nil { 1318 return false, err 1319 } 1320 } 1321 if !GitRefIsSHA(b) { 1322 var err error 1323 b, err = GitRefToFullSHA(b) 1324 if err != nil { 1325 return false, err 1326 } 1327 } 1328 cmd := exec.Command("git", "merge-base", a, b) 1329 outp, err := cmd.Output() 1330 if err != nil { 1331 return false, err 1332 } 1333 base := strings.TrimSpace(string(outp)) 1334 1335 return base == a, nil 1336 1337 } 1338 1339 // Returns the 'best' ancestor of all the passed in refs (as a SHA) 1340 // If a ref is listed twice the 'best' ancestor will be itself 1341 func GetGitBestAncestor(refs []string) (ancestor string, err error) { 1342 args := []string{"merge-base"} 1343 args = append(args, refs...) 1344 cmd := exec.Command("git", args...) 1345 outp, err := cmd.Output() 1346 if err != nil { 1347 return "", err 1348 } 1349 base := strings.TrimSpace(string(outp)) 1350 return base, nil 1351 } 1352 1353 // Gets the latest change to a specific LOB file at ref, returning the SHA and the commit details 1354 func GetGitLatestLOBChangeDetails(filename, ref string) (summary *GitCommitSummary, lobsha string, err error) { 1355 cmd := exec.Command("git", "log", "-p", 1356 "-n", "1", // one commit 1357 "-G", SHALineRegexStr, // if this file was ever embedded verbatim, ignore those 1358 `--format=commit:%H|%h|%P|%ai|%ci|%ae|%an|%ce|%cn|%s`, // standard summary info 1359 ref, "--", filename) 1360 outp, err := cmd.StdoutPipe() 1361 if err != nil { 1362 return nil, "", errors.New(fmt.Sprintf("Unable to get latest commit from %v: %v", ref, err.Error())) 1363 } 1364 cmd.Start() 1365 scanner := bufio.NewScanner(outp) 1366 summary = &GitCommitSummary{} 1367 lobsha = "" 1368 lobsharegex := regexp.MustCompile(`^\+git-lob: ([A-Fa-f0-9]{40})`) 1369 err = nil 1370 for scanner.Scan() { 1371 line := scanner.Text() 1372 if strings.HasPrefix(line, "commit:") { 1373 // At most 10 substrings so subject line is not split on anything 1374 fields := strings.SplitN(string(line[7:]), "|", 10) 1375 // Cope with the case where subject is blank 1376 if len(fields) >= 9 { 1377 // Get SHAs from output, not commit input, so we can support symbolic refs 1378 summary.SHA = fields[0] 1379 summary.ShortSHA = fields[1] 1380 summary.Parents = strings.Split(fields[2], " ") 1381 // %aD & %cD (RFC2822) matches Go's RFC1123Z format 1382 summary.AuthorDate, _ = ParseGitDate(fields[3]) 1383 summary.CommitDate, _ = ParseGitDate(fields[4]) 1384 summary.AuthorEmail = fields[5] 1385 summary.AuthorName = fields[6] 1386 summary.CommitterEmail = fields[7] 1387 summary.CommitterName = fields[8] 1388 if len(fields) > 9 { 1389 summary.Subject = strings.TrimRight(fields[9], "\n") 1390 } 1391 } else { 1392 msg := fmt.Sprintf("Unexpected output from git log: %v", line) 1393 return nil, "", errors.New(msg) 1394 } 1395 } else if match := lobsharegex.FindStringSubmatch(line); match != nil { 1396 lobsha = match[1] 1397 } 1398 } 1399 return 1400 1401 }