1// Package git contains various commands that shell out to git 2// NOTE: Subject to change, do not rely on this package from outside git-lfs source 3package git 4 5import ( 6 "bufio" 7 "bytes" 8 "crypto/sha1" 9 "crypto/sha256" 10 "encoding/hex" 11 "errors" 12 "fmt" 13 "io" 14 "io/ioutil" 15 "net/url" 16 "os" 17 "os/exec" 18 "path/filepath" 19 "regexp" 20 "strconv" 21 "strings" 22 "sync" 23 "syscall" 24 "time" 25 26 lfserrors "github.com/git-lfs/git-lfs/v3/errors" 27 "github.com/git-lfs/git-lfs/v3/subprocess" 28 "github.com/git-lfs/git-lfs/v3/tools" 29 "github.com/git-lfs/gitobj/v2" 30 "github.com/rubyist/tracerx" 31) 32 33type RefType int 34 35const ( 36 RefTypeLocalBranch = RefType(iota) 37 RefTypeRemoteBranch = RefType(iota) 38 RefTypeLocalTag = RefType(iota) 39 RefTypeRemoteTag = RefType(iota) 40 RefTypeHEAD = RefType(iota) // current checkout 41 RefTypeOther = RefType(iota) // stash or unknown 42 43 SHA1HexSize = sha1.Size * 2 44 SHA256HexSize = sha256.Size * 2 45) 46 47var ( 48 ObjectIDRegex = fmt.Sprintf("(?:[0-9a-f]{%d}(?:[0-9a-f]{%d})?)", SHA1HexSize, SHA256HexSize-SHA1HexSize) 49 // ObjectIDLengths is a slice of valid Git hexadecimal object ID 50 // lengths in increasing order. 51 ObjectIDLengths = []int{SHA1HexSize, SHA256HexSize} 52 emptyTree = "" 53 emptyTreeMutex = &sync.Mutex{} 54) 55 56type IndexStage int 57 58const ( 59 IndexStageDefault IndexStage = iota 60 IndexStageBase 61 IndexStageOurs 62 IndexStageTheirs 63) 64 65// Prefix returns the given RefType's prefix, "refs/heads", "ref/remotes", 66// etc. It returns an additional value of either true/false, whether or not this 67// given ref type has a prefix. 68// 69// If the RefType is unrecognized, Prefix() will panic. 70func (t RefType) Prefix() (string, bool) { 71 switch t { 72 case RefTypeLocalBranch: 73 return "refs/heads", true 74 case RefTypeRemoteBranch: 75 return "refs/remotes", true 76 case RefTypeLocalTag: 77 return "refs/tags", true 78 default: 79 return "", false 80 } 81} 82 83func ParseRef(absRef, sha string) *Ref { 84 r := &Ref{Sha: sha} 85 if strings.HasPrefix(absRef, "refs/heads/") { 86 r.Name = absRef[11:] 87 r.Type = RefTypeLocalBranch 88 } else if strings.HasPrefix(absRef, "refs/tags/") { 89 r.Name = absRef[10:] 90 r.Type = RefTypeLocalTag 91 } else if strings.HasPrefix(absRef, "refs/remotes/") { 92 r.Name = absRef[13:] 93 r.Type = RefTypeRemoteBranch 94 } else { 95 r.Name = absRef 96 if absRef == "HEAD" { 97 r.Type = RefTypeHEAD 98 } else { 99 r.Type = RefTypeOther 100 } 101 } 102 return r 103} 104 105// A git reference (branch, tag etc) 106type Ref struct { 107 Name string 108 Type RefType 109 Sha string 110} 111 112// Refspec returns the fully-qualified reference name (including remote), i.e., 113// for a remote branch called 'my-feature' on remote 'origin', this function 114// will return: 115// 116// refs/remotes/origin/my-feature 117func (r *Ref) Refspec() string { 118 if r == nil { 119 return "" 120 } 121 122 prefix, ok := r.Type.Prefix() 123 if ok { 124 return fmt.Sprintf("%s/%s", prefix, r.Name) 125 } 126 127 return r.Name 128} 129 130// HasValidObjectIDLength returns true if `s` has a length that is a valid 131// hexadecimal Git object ID length. 132func HasValidObjectIDLength(s string) bool { 133 for _, length := range ObjectIDLengths { 134 if len(s) == length { 135 return true 136 } 137 } 138 return false 139} 140 141// IsZeroObjectID returns true if the string is a valid hexadecimal Git object 142// ID and represents the all-zeros object ID for some hash algorithm. 143func IsZeroObjectID(s string) bool { 144 for _, length := range ObjectIDLengths { 145 if s == strings.Repeat("0", length) { 146 return true 147 } 148 } 149 return false 150} 151 152func EmptyTree() string { 153 emptyTreeMutex.Lock() 154 defer emptyTreeMutex.Unlock() 155 156 if len(emptyTree) == 0 { 157 cmd := gitNoLFS("hash-object", "-t", "tree", "/dev/null") 158 cmd.Stdin = nil 159 out, _ := cmd.Output() 160 emptyTree = strings.TrimSpace(string(out)) 161 } 162 return emptyTree 163} 164 165// Some top level information about a commit (only first line of message) 166type CommitSummary struct { 167 Sha string 168 ShortSha string 169 Parents []string 170 CommitDate time.Time 171 AuthorDate time.Time 172 AuthorName string 173 AuthorEmail string 174 CommitterName string 175 CommitterEmail string 176 Subject string 177} 178 179// Prepend Git config instructions to disable Git LFS filter 180func gitConfigNoLFS(args ...string) []string { 181 // Before git 2.8, setting filters to blank causes lots of warnings, so use cat instead (slightly slower) 182 // Also pre 2.2 it failed completely. We used to use it anyway in git 2.2-2.7 and 183 // suppress the messages in stderr, but doing that with standard StderrPipe suppresses 184 // the git clone output (git thinks it's not a terminal) and makes it look like it's 185 // not working. You can get around that with https://github.com/kr/pty but that 186 // causes difficult issues with passing through Stdin for login prompts 187 // This way is simpler & more practical. 188 filterOverride := "" 189 if !IsGitVersionAtLeast("2.8.0") { 190 filterOverride = "cat" 191 } 192 193 return append([]string{ 194 "-c", fmt.Sprintf("filter.lfs.smudge=%v", filterOverride), 195 "-c", fmt.Sprintf("filter.lfs.clean=%v", filterOverride), 196 "-c", "filter.lfs.process=", 197 "-c", "filter.lfs.required=false", 198 }, args...) 199} 200 201// Invoke Git with disabled LFS filters 202func gitNoLFS(args ...string) *subprocess.Cmd { 203 return subprocess.ExecCommand("git", gitConfigNoLFS(args...)...) 204} 205 206func gitNoLFSSimple(args ...string) (string, error) { 207 return subprocess.SimpleExec("git", gitConfigNoLFS(args...)...) 208} 209 210func gitNoLFSBuffered(args ...string) (*subprocess.BufferedCmd, error) { 211 return subprocess.BufferedExec("git", gitConfigNoLFS(args...)...) 212} 213 214// Invoke Git with enabled LFS filters 215func git(args ...string) *subprocess.Cmd { 216 return subprocess.ExecCommand("git", args...) 217} 218 219func gitSimple(args ...string) (string, error) { 220 return subprocess.SimpleExec("git", args...) 221} 222 223func gitBuffered(args ...string) (*subprocess.BufferedCmd, error) { 224 return subprocess.BufferedExec("git", args...) 225} 226 227func CatFile() (*subprocess.BufferedCmd, error) { 228 return gitNoLFSBuffered("cat-file", "--batch-check") 229} 230 231func DiffIndex(ref string, cached bool, refresh bool) (*bufio.Scanner, error) { 232 if refresh { 233 _, err := gitSimple("update-index", "-q", "--refresh") 234 if err != nil { 235 return nil, lfserrors.Wrap(err, "Failed to run git update-index") 236 } 237 } 238 239 args := []string{"diff-index", "-M"} 240 if cached { 241 args = append(args, "--cached") 242 } 243 args = append(args, ref) 244 245 cmd, err := gitBuffered(args...) 246 if err != nil { 247 return nil, err 248 } 249 if err = cmd.Stdin.Close(); err != nil { 250 return nil, err 251 } 252 253 return bufio.NewScanner(cmd.Stdout), nil 254} 255 256func HashObject(r io.Reader) (string, error) { 257 cmd := gitNoLFS("hash-object", "--stdin") 258 cmd.Stdin = r 259 out, err := cmd.Output() 260 if err != nil { 261 return "", fmt.Errorf("error building Git blob OID: %s", err) 262 } 263 264 return string(bytes.TrimSpace(out)), nil 265} 266 267func Log(args ...string) (*subprocess.BufferedCmd, error) { 268 logArgs := append([]string{"log"}, args...) 269 return gitNoLFSBuffered(logArgs...) 270} 271 272func LsRemote(remote, remoteRef string) (string, error) { 273 if remote == "" { 274 return "", errors.New("remote required") 275 } 276 if remoteRef == "" { 277 return gitNoLFSSimple("ls-remote", remote) 278 279 } 280 return gitNoLFSSimple("ls-remote", remote, remoteRef) 281} 282 283func LsTree(ref string) (*subprocess.BufferedCmd, error) { 284 return gitNoLFSBuffered( 285 "ls-tree", 286 "-r", // recurse 287 "-l", // report object size (we'll need this) 288 "-z", // null line termination 289 "--full-tree", // start at the root regardless of where we are in it 290 ref, 291 ) 292} 293 294func ResolveRef(ref string) (*Ref, error) { 295 outp, err := gitNoLFSSimple("rev-parse", ref, "--symbolic-full-name", ref) 296 if err != nil { 297 return nil, fmt.Errorf("Git can't resolve ref: %q", ref) 298 } 299 if outp == "" { 300 return nil, fmt.Errorf("Git can't resolve ref: %q", ref) 301 } 302 303 lines := strings.Split(outp, "\n") 304 fullref := &Ref{Sha: lines[0]} 305 306 if len(lines) == 1 { 307 // ref is a sha1 and has no symbolic-full-name 308 fullref.Name = lines[0] 309 fullref.Sha = lines[0] 310 fullref.Type = RefTypeOther 311 return fullref, nil 312 } 313 314 // parse the symbolic-full-name 315 fullref.Type, fullref.Name = ParseRefToTypeAndName(lines[1]) 316 return fullref, nil 317} 318 319func ResolveRefs(refnames []string) ([]*Ref, error) { 320 refs := make([]*Ref, len(refnames)) 321 for i, name := range refnames { 322 ref, err := ResolveRef(name) 323 if err != nil { 324 return refs, err 325 } 326 327 refs[i] = ref 328 } 329 return refs, nil 330} 331 332func CurrentRef() (*Ref, error) { 333 return ResolveRef("HEAD") 334} 335 336func (c *Configuration) CurrentRemoteRef() (*Ref, error) { 337 remoteref, err := c.RemoteRefNameForCurrentBranch() 338 if err != nil { 339 return nil, err 340 } 341 342 return ResolveRef(remoteref) 343} 344 345// RemoteRefForCurrentBranch returns the full remote ref (refs/remotes/{remote}/{remotebranch}) 346// that the current branch is tracking. 347func (c *Configuration) RemoteRefNameForCurrentBranch() (string, error) { 348 ref, err := CurrentRef() 349 if err != nil { 350 return "", err 351 } 352 353 if ref.Type == RefTypeHEAD || ref.Type == RefTypeOther { 354 return "", errors.New("not on a branch") 355 } 356 357 remote := c.RemoteForBranch(ref.Name) 358 if remote == "" { 359 return "", fmt.Errorf("remote not found for branch %q", ref.Name) 360 } 361 362 remotebranch := c.RemoteBranchForLocalBranch(ref.Name) 363 364 return fmt.Sprintf("refs/remotes/%s/%s", remote, remotebranch), nil 365} 366 367// RemoteForBranch returns the remote name that a given local branch is tracking (blank if none) 368func (c *Configuration) RemoteForBranch(localBranch string) string { 369 return c.Find(fmt.Sprintf("branch.%s.remote", localBranch)) 370} 371 372// RemoteBranchForLocalBranch returns the name (only) of the remote branch that the local branch is tracking 373// If no specific branch is configured, returns local branch name 374func (c *Configuration) RemoteBranchForLocalBranch(localBranch string) string { 375 // get remote ref to track, may not be same name 376 merge := c.Find(fmt.Sprintf("branch.%s.merge", localBranch)) 377 if strings.HasPrefix(merge, "refs/heads/") { 378 return merge[11:] 379 } else { 380 return localBranch 381 } 382} 383 384func RemoteList() ([]string, error) { 385 cmd := gitNoLFS("remote") 386 387 outp, err := cmd.StdoutPipe() 388 if err != nil { 389 return nil, fmt.Errorf("failed to call git remote: %v", err) 390 } 391 cmd.Start() 392 defer cmd.Wait() 393 394 scanner := bufio.NewScanner(outp) 395 396 var ret []string 397 for scanner.Scan() { 398 ret = append(ret, strings.TrimSpace(scanner.Text())) 399 } 400 401 return ret, nil 402} 403 404func RemoteURLs(push bool) (map[string][]string, error) { 405 cmd := gitNoLFS("remote", "-v") 406 407 outp, err := cmd.StdoutPipe() 408 if err != nil { 409 return nil, fmt.Errorf("failed to call git remote -v: %v", err) 410 } 411 cmd.Start() 412 defer cmd.Wait() 413 414 scanner := bufio.NewScanner(outp) 415 416 text := "(fetch)" 417 if push { 418 text = "(push)" 419 } 420 ret := make(map[string][]string) 421 for scanner.Scan() { 422 // [remote, urlpair-text] 423 pair := strings.Split(strings.TrimSpace(scanner.Text()), "\t") 424 if len(pair) != 2 { 425 continue 426 } 427 // [url, "(fetch)" | "(push)"] 428 urlpair := strings.Split(pair[1], " ") 429 if len(urlpair) != 2 || urlpair[1] != text { 430 continue 431 } 432 ret[pair[0]] = append(ret[pair[0]], urlpair[0]) 433 } 434 435 return ret, nil 436} 437 438func MapRemoteURL(url string, push bool) (string, bool) { 439 urls, err := RemoteURLs(push) 440 if err != nil { 441 return url, false 442 } 443 444 for name, remotes := range urls { 445 if len(remotes) == 1 && url == remotes[0] { 446 return name, true 447 } 448 } 449 return url, false 450} 451 452// Refs returns all of the local and remote branches and tags for the current 453// repository. Other refs (HEAD, refs/stash, git notes) are ignored. 454func LocalRefs() ([]*Ref, error) { 455 cmd := gitNoLFS("show-ref") 456 457 outp, err := cmd.StdoutPipe() 458 if err != nil { 459 return nil, fmt.Errorf("failed to call git show-ref: %v", err) 460 } 461 462 var refs []*Ref 463 464 if err := cmd.Start(); err != nil { 465 return refs, err 466 } 467 468 scanner := bufio.NewScanner(outp) 469 for scanner.Scan() { 470 line := strings.TrimSpace(scanner.Text()) 471 parts := strings.SplitN(line, " ", 2) 472 if len(parts) != 2 || !HasValidObjectIDLength(parts[0]) || len(parts[1]) < 1 { 473 tracerx.Printf("Invalid line from git show-ref: %q", line) 474 continue 475 } 476 477 rtype, name := ParseRefToTypeAndName(parts[1]) 478 if rtype != RefTypeLocalBranch && rtype != RefTypeLocalTag { 479 continue 480 } 481 482 refs = append(refs, &Ref{name, rtype, parts[0]}) 483 } 484 485 return refs, cmd.Wait() 486} 487 488// UpdateRef moves the given ref to a new sha with a given reason (and creates a 489// reflog entry, if a "reason" was provided). It returns an error if any were 490// encountered. 491func UpdateRef(ref *Ref, to []byte, reason string) error { 492 return UpdateRefIn("", ref, to, reason) 493} 494 495// UpdateRef moves the given ref to a new sha with a given reason (and creates a 496// reflog entry, if a "reason" was provided). It operates within the given 497// working directory "wd". It returns an error if any were encountered. 498func UpdateRefIn(wd string, ref *Ref, to []byte, reason string) error { 499 args := []string{"update-ref", ref.Refspec(), hex.EncodeToString(to)} 500 if len(reason) > 0 { 501 args = append(args, "-m", reason) 502 } 503 504 cmd := gitNoLFS(args...) 505 cmd.Dir = wd 506 507 return cmd.Run() 508} 509 510// ValidateRemote checks that a named remote is valid for use 511// Mainly to check user-supplied remotes & fail more nicely 512func ValidateRemote(remote string) error { 513 remotes, err := RemoteList() 514 if err != nil { 515 return err 516 } 517 for _, r := range remotes { 518 if r == remote { 519 return nil 520 } 521 } 522 523 if err = ValidateRemoteURL(remote); err == nil { 524 return nil 525 } 526 527 return fmt.Errorf("invalid remote name: %q", remote) 528} 529 530// ValidateRemoteURL checks that a string is a valid Git remote URL 531func ValidateRemoteURL(remote string) error { 532 u, _ := url.Parse(remote) 533 if u == nil || u.Scheme == "" { 534 // This is either an invalid remote name (maybe the user made a typo 535 // when selecting a named remote) or a bare SSH URL like 536 // "x@y.com:path/to/resource.git". Guess that this is a URL in the latter 537 // form if the string contains a colon ":", and an invalid remote if it 538 // does not. 539 if strings.Contains(remote, ":") { 540 return nil 541 } else { 542 return fmt.Errorf("invalid remote name: %q", remote) 543 } 544 } 545 546 switch u.Scheme { 547 case "ssh", "http", "https", "git", "file": 548 return nil 549 default: 550 return fmt.Errorf("invalid remote url protocol %q in %q", u.Scheme, remote) 551 } 552} 553 554func RewriteLocalPathAsURL(path string) string { 555 var slash string 556 if abs, err := filepath.Abs(path); err == nil { 557 // Required for Windows paths to work. 558 if !strings.HasPrefix(abs, "/") { 559 slash = "/" 560 } 561 path = abs 562 } 563 564 var gitpath string 565 if filepath.Base(path) == ".git" { 566 gitpath = path 567 path = filepath.Dir(path) 568 } else { 569 gitpath = filepath.Join(path, ".git") 570 } 571 572 if _, err := os.Stat(gitpath); err == nil { 573 path = gitpath 574 } else if _, err := os.Stat(path); err != nil { 575 // Not a local path. We check down here because we perform 576 // canonicalization by stripping off the .git above. 577 return path 578 } 579 return fmt.Sprintf("file://%s%s", slash, filepath.ToSlash(path)) 580} 581 582func UpdateIndexFromStdin() *subprocess.Cmd { 583 return git("update-index", "-q", "--refresh", "--stdin") 584} 585 586// RecentBranches returns branches with commit dates on or after the given date/time 587// Return full Ref type for easier detection of duplicate SHAs etc 588// since: refs with commits on or after this date will be included 589// includeRemoteBranches: true to include refs on remote branches 590// onlyRemote: set to non-blank to only include remote branches on a single remote 591func RecentBranches(since time.Time, includeRemoteBranches bool, onlyRemote string) ([]*Ref, error) { 592 cmd := gitNoLFS("for-each-ref", 593 `--sort=-committerdate`, 594 `--format=%(refname) %(objectname) %(committerdate:iso)`, 595 "refs") 596 outp, err := cmd.StdoutPipe() 597 if err != nil { 598 return nil, fmt.Errorf("failed to call git for-each-ref: %v", err) 599 } 600 cmd.Start() 601 defer cmd.Wait() 602 603 scanner := bufio.NewScanner(outp) 604 605 // Output is like this: 606 // refs/heads/master f03686b324b29ff480591745dbfbbfa5e5ac1bd5 2015-08-19 16:50:37 +0100 607 // refs/remotes/origin/master ad3b29b773e46ad6870fdf08796c33d97190fe93 2015-08-13 16:50:37 +0100 608 609 // Output is ordered by latest commit date first, so we can stop at the threshold 610 regex := regexp.MustCompile(fmt.Sprintf(`^(refs/[^/]+/\S+)\s+(%s)\s+(\d{4}-\d{2}-\d{2}\s+\d{2}\:\d{2}\:\d{2}\s+[\+\-]\d{4})`, ObjectIDRegex)) 611 tracerx.Printf("RECENT: Getting refs >= %v", since) 612 var ret []*Ref 613 for scanner.Scan() { 614 line := scanner.Text() 615 if match := regex.FindStringSubmatch(line); match != nil { 616 fullref := match[1] 617 sha := match[2] 618 reftype, ref := ParseRefToTypeAndName(fullref) 619 if reftype == RefTypeRemoteBranch { 620 if !includeRemoteBranches { 621 continue 622 } 623 if onlyRemote != "" && !strings.HasPrefix(ref, onlyRemote+"/") { 624 continue 625 } 626 } 627 // This is a ref we might use 628 // Check the date 629 commitDate, err := ParseGitDate(match[3]) 630 if err != nil { 631 return ret, err 632 } 633 if commitDate.Before(since) { 634 // the end 635 break 636 } 637 tracerx.Printf("RECENT: %v (%v)", ref, commitDate) 638 ret = append(ret, &Ref{ref, reftype, sha}) 639 } 640 } 641 642 return ret, nil 643 644} 645 646// Get the type & name of a git reference 647func ParseRefToTypeAndName(fullref string) (t RefType, name string) { 648 const localPrefix = "refs/heads/" 649 const remotePrefix = "refs/remotes/" 650 const localTagPrefix = "refs/tags/" 651 652 if fullref == "HEAD" { 653 name = fullref 654 t = RefTypeHEAD 655 } else if strings.HasPrefix(fullref, localPrefix) { 656 name = fullref[len(localPrefix):] 657 t = RefTypeLocalBranch 658 } else if strings.HasPrefix(fullref, remotePrefix) { 659 name = fullref[len(remotePrefix):] 660 t = RefTypeRemoteBranch 661 } else if strings.HasPrefix(fullref, localTagPrefix) { 662 name = fullref[len(localTagPrefix):] 663 t = RefTypeLocalTag 664 } else { 665 name = fullref 666 t = RefTypeOther 667 } 668 return 669} 670 671// Parse a Git date formatted in ISO 8601 format (%ci/%ai) 672func ParseGitDate(str string) (time.Time, error) { 673 674 // Unfortunately Go and Git don't overlap in their builtin date formats 675 // Go's time.RFC1123Z and Git's %cD are ALMOST the same, except that 676 // when the day is < 10 Git outputs a single digit, but Go expects a leading 677 // zero - this is enough to break the parsing. Sigh. 678 679 // Format is for 2 Jan 2006, 15:04:05 -7 UTC as per Go 680 return time.Parse("2006-01-02 15:04:05 -0700", str) 681} 682 683// FormatGitDate converts a Go date into a git command line format date 684func FormatGitDate(tm time.Time) string { 685 // Git format is "Fri Jun 21 20:26:41 2013 +0900" but no zero-leading for day 686 return tm.Format("Mon Jan 2 15:04:05 2006 -0700") 687} 688 689// Get summary information about a commit 690func GetCommitSummary(commit string) (*CommitSummary, error) { 691 cmd := gitNoLFS("show", "-s", 692 `--format=%H|%h|%P|%ai|%ci|%ae|%an|%ce|%cn|%s`, commit) 693 694 out, err := cmd.CombinedOutput() 695 if err != nil { 696 return nil, fmt.Errorf("failed to call git show: %v %v", err, string(out)) 697 } 698 699 // At most 10 substrings so subject line is not split on anything 700 fields := strings.SplitN(string(out), "|", 10) 701 // Cope with the case where subject is blank 702 if len(fields) >= 9 { 703 ret := &CommitSummary{} 704 // Get SHAs from output, not commit input, so we can support symbolic refs 705 ret.Sha = fields[0] 706 ret.ShortSha = fields[1] 707 ret.Parents = strings.Split(fields[2], " ") 708 // %aD & %cD (RFC2822) matches Go's RFC1123Z format 709 ret.AuthorDate, _ = ParseGitDate(fields[3]) 710 ret.CommitDate, _ = ParseGitDate(fields[4]) 711 ret.AuthorEmail = fields[5] 712 ret.AuthorName = fields[6] 713 ret.CommitterEmail = fields[7] 714 ret.CommitterName = fields[8] 715 if len(fields) > 9 { 716 ret.Subject = strings.TrimRight(fields[9], "\n") 717 } 718 return ret, nil 719 } else { 720 msg := fmt.Sprintf("Unexpected output from git show: %v", string(out)) 721 return nil, errors.New(msg) 722 } 723} 724 725func GitAndRootDirs() (string, string, error) { 726 cmd := gitNoLFS("rev-parse", "--git-dir", "--show-toplevel") 727 buf := &bytes.Buffer{} 728 cmd.Stderr = buf 729 730 out, err := cmd.Output() 731 output := string(out) 732 if err != nil { 733 // If we got a fatal error, it's possible we're on a newer 734 // (2.24+) Git and we're not in a worktree, so fall back to just 735 // looking up the repo directory. 736 if e, ok := err.(*exec.ExitError); ok { 737 var ws syscall.WaitStatus 738 ws, ok = e.ProcessState.Sys().(syscall.WaitStatus) 739 if ok && ws.ExitStatus() == 128 { 740 absGitDir, err := GitDir() 741 return absGitDir, "", err 742 } 743 } 744 return "", "", fmt.Errorf("failed to call git rev-parse --git-dir --show-toplevel: %q", buf.String()) 745 } 746 747 paths := strings.Split(output, "\n") 748 pathLen := len(paths) 749 750 if pathLen == 0 { 751 return "", "", fmt.Errorf("bad git rev-parse output: %q", output) 752 } 753 754 absGitDir, err := tools.CanonicalizePath(paths[0], false) 755 if err != nil { 756 return "", "", fmt.Errorf("error converting %q to absolute: %s", paths[0], err) 757 } 758 759 if pathLen == 1 || len(paths[1]) == 0 { 760 return absGitDir, "", nil 761 } 762 763 absRootDir, err := tools.CanonicalizePath(paths[1], false) 764 return absGitDir, absRootDir, err 765} 766 767func RootDir() (string, error) { 768 cmd := gitNoLFS("rev-parse", "--show-toplevel") 769 out, err := cmd.Output() 770 if err != nil { 771 return "", fmt.Errorf("failed to call git rev-parse --show-toplevel: %v %v", err, string(out)) 772 } 773 774 path := strings.TrimSpace(string(out)) 775 path, err = tools.TranslateCygwinPath(path) 776 if err != nil { 777 return "", err 778 } 779 return tools.CanonicalizePath(path, false) 780} 781 782func GitDir() (string, error) { 783 cmd := gitNoLFS("rev-parse", "--git-dir") 784 buf := &bytes.Buffer{} 785 cmd.Stderr = buf 786 out, err := cmd.Output() 787 788 if err != nil { 789 return "", fmt.Errorf("failed to call git rev-parse --git-dir: %v %v: %v", err, string(out), buf.String()) 790 } 791 path := strings.TrimSpace(string(out)) 792 return tools.CanonicalizePath(path, false) 793} 794 795func GitCommonDir() (string, error) { 796 // Versions before 2.5.0 don't have the --git-common-dir option, since 797 // it came in with worktrees, so just fall back to the main Git 798 // directory. 799 if !IsGitVersionAtLeast("2.5.0") { 800 return GitDir() 801 } 802 803 cmd := gitNoLFS("rev-parse", "--git-common-dir") 804 out, err := cmd.Output() 805 buf := &bytes.Buffer{} 806 cmd.Stderr = buf 807 if err != nil { 808 return "", fmt.Errorf("failed to call git rev-parse --git-common-dir: %v %v: %v", err, string(out), buf.String()) 809 } 810 path := strings.TrimSpace(string(out)) 811 path, err = tools.TranslateCygwinPath(path) 812 if err != nil { 813 return "", err 814 } 815 return tools.CanonicalizePath(path, false) 816} 817 818// GetAllWorkTreeHEADs returns the refs that all worktrees are using as HEADs 819// This returns all worktrees plus the master working copy, and works even if 820// working dir is actually in a worktree right now 821// Pass in the git storage dir (parent of 'objects') to work from 822func GetAllWorkTreeHEADs(storageDir string) ([]*Ref, error) { 823 worktreesdir := filepath.Join(storageDir, "worktrees") 824 dirf, err := os.Open(worktreesdir) 825 if err != nil && !os.IsNotExist(err) { 826 return nil, err 827 } 828 829 var worktrees []*Ref 830 if err == nil { 831 // There are some worktrees 832 defer dirf.Close() 833 direntries, err := dirf.Readdir(0) 834 if err != nil { 835 return nil, err 836 } 837 for _, dirfi := range direntries { 838 if dirfi.IsDir() { 839 // to avoid having to chdir and run git commands to identify the commit 840 // just read the HEAD file & git rev-parse if necessary 841 // Since the git repo is shared the same rev-parse will work from this location 842 headfile := filepath.Join(worktreesdir, dirfi.Name(), "HEAD") 843 ref, err := parseRefFile(headfile) 844 if err != nil { 845 tracerx.Printf("Error reading %v for worktree, skipping: %v", headfile, err) 846 continue 847 } 848 worktrees = append(worktrees, ref) 849 } 850 } 851 } 852 853 // This has only established the separate worktrees, not the original checkout 854 // If the storageDir contains a HEAD file then there is a main checkout 855 // as well; this mus tbe resolveable whether you're in the main checkout or 856 // a worktree 857 headfile := filepath.Join(storageDir, "HEAD") 858 ref, err := parseRefFile(headfile) 859 if err == nil { 860 worktrees = append(worktrees, ref) 861 } else if !os.IsNotExist(err) { // ok if not exists, probably bare repo 862 tracerx.Printf("Error reading %v for main checkout, skipping: %v", headfile, err) 863 } 864 865 return worktrees, nil 866} 867 868// Manually parse a reference file like HEAD and return the Ref it resolves to 869func parseRefFile(filename string) (*Ref, error) { 870 bytes, err := ioutil.ReadFile(filename) 871 if err != nil { 872 return nil, err 873 } 874 contents := strings.TrimSpace(string(bytes)) 875 if strings.HasPrefix(contents, "ref:") { 876 contents = strings.TrimSpace(contents[4:]) 877 } 878 return ResolveRef(contents) 879} 880 881// IsBare returns whether or not a repository is bare. It requires that the 882// current working directory is a repository. 883// 884// If there was an error determining whether or not the repository is bare, it 885// will be returned. 886func IsBare() (bool, error) { 887 s, err := subprocess.SimpleExec( 888 "git", "rev-parse", "--is-bare-repository") 889 890 if err != nil { 891 return false, err 892 } 893 894 return strconv.ParseBool(s) 895} 896 897// For compatibility with git clone we must mirror all flags in CloneWithoutFilters 898type CloneFlags struct { 899 // --template <template_directory> 900 TemplateDirectory string 901 // -l --local 902 Local bool 903 // -s --shared 904 Shared bool 905 // --no-hardlinks 906 NoHardlinks bool 907 // -q --quiet 908 Quiet bool 909 // -n --no-checkout 910 NoCheckout bool 911 // --progress 912 Progress bool 913 // --bare 914 Bare bool 915 // --mirror 916 Mirror bool 917 // -o <name> --origin <name> 918 Origin string 919 // -b <name> --branch <name> 920 Branch string 921 // -u <upload-pack> --upload-pack <pack> 922 Upload string 923 // --reference <repository> 924 Reference string 925 // --reference-if-able <repository> 926 ReferenceIfAble string 927 // --dissociate 928 Dissociate bool 929 // --separate-git-dir <git dir> 930 SeparateGit string 931 // --depth <depth> 932 Depth string 933 // --recursive 934 Recursive bool 935 // --recurse-submodules 936 RecurseSubmodules bool 937 // -c <value> --config <value> 938 Config string 939 // --single-branch 940 SingleBranch bool 941 // --no-single-branch 942 NoSingleBranch bool 943 // --verbose 944 Verbose bool 945 // --ipv4 946 Ipv4 bool 947 // --ipv6 948 Ipv6 bool 949 // --shallow-since <date> 950 ShallowSince string 951 // --shallow-since <date> 952 ShallowExclude string 953 // --shallow-submodules 954 ShallowSubmodules bool 955 // --no-shallow-submodules 956 NoShallowSubmodules bool 957 // jobs <n> 958 Jobs int64 959} 960 961// CloneWithoutFilters clones a git repo but without the smudge filter enabled 962// so that files in the working copy will be pointers and not real LFS data 963func CloneWithoutFilters(flags CloneFlags, args []string) error { 964 965 cmdargs := []string{"clone"} 966 967 // flags 968 if flags.Bare { 969 cmdargs = append(cmdargs, "--bare") 970 } 971 if len(flags.Branch) > 0 { 972 cmdargs = append(cmdargs, "--branch", flags.Branch) 973 } 974 if len(flags.Config) > 0 { 975 cmdargs = append(cmdargs, "--config", flags.Config) 976 } 977 if len(flags.Depth) > 0 { 978 cmdargs = append(cmdargs, "--depth", flags.Depth) 979 } 980 if flags.Dissociate { 981 cmdargs = append(cmdargs, "--dissociate") 982 } 983 if flags.Ipv4 { 984 cmdargs = append(cmdargs, "--ipv4") 985 } 986 if flags.Ipv6 { 987 cmdargs = append(cmdargs, "--ipv6") 988 } 989 if flags.Local { 990 cmdargs = append(cmdargs, "--local") 991 } 992 if flags.Mirror { 993 cmdargs = append(cmdargs, "--mirror") 994 } 995 if flags.NoCheckout { 996 cmdargs = append(cmdargs, "--no-checkout") 997 } 998 if flags.NoHardlinks { 999 cmdargs = append(cmdargs, "--no-hardlinks") 1000 } 1001 if flags.NoSingleBranch { 1002 cmdargs = append(cmdargs, "--no-single-branch") 1003 } 1004 if len(flags.Origin) > 0 { 1005 cmdargs = append(cmdargs, "--origin", flags.Origin) 1006 } 1007 if flags.Progress { 1008 cmdargs = append(cmdargs, "--progress") 1009 } 1010 if flags.Quiet { 1011 cmdargs = append(cmdargs, "--quiet") 1012 } 1013 if flags.Recursive { 1014 cmdargs = append(cmdargs, "--recursive") 1015 } 1016 if flags.RecurseSubmodules { 1017 cmdargs = append(cmdargs, "--recurse-submodules") 1018 } 1019 if len(flags.Reference) > 0 { 1020 cmdargs = append(cmdargs, "--reference", flags.Reference) 1021 } 1022 if len(flags.ReferenceIfAble) > 0 { 1023 cmdargs = append(cmdargs, "--reference-if-able", flags.ReferenceIfAble) 1024 } 1025 if len(flags.SeparateGit) > 0 { 1026 cmdargs = append(cmdargs, "--separate-git-dir", flags.SeparateGit) 1027 } 1028 if flags.Shared { 1029 cmdargs = append(cmdargs, "--shared") 1030 } 1031 if flags.SingleBranch { 1032 cmdargs = append(cmdargs, "--single-branch") 1033 } 1034 if len(flags.TemplateDirectory) > 0 { 1035 cmdargs = append(cmdargs, "--template", flags.TemplateDirectory) 1036 } 1037 if len(flags.Upload) > 0 { 1038 cmdargs = append(cmdargs, "--upload-pack", flags.Upload) 1039 } 1040 if flags.Verbose { 1041 cmdargs = append(cmdargs, "--verbose") 1042 } 1043 if len(flags.ShallowSince) > 0 { 1044 cmdargs = append(cmdargs, "--shallow-since", flags.ShallowSince) 1045 } 1046 if len(flags.ShallowExclude) > 0 { 1047 cmdargs = append(cmdargs, "--shallow-exclude", flags.ShallowExclude) 1048 } 1049 if flags.ShallowSubmodules { 1050 cmdargs = append(cmdargs, "--shallow-submodules") 1051 } 1052 if flags.NoShallowSubmodules { 1053 cmdargs = append(cmdargs, "--no-shallow-submodules") 1054 } 1055 if flags.Jobs > -1 { 1056 cmdargs = append(cmdargs, "--jobs", strconv.FormatInt(flags.Jobs, 10)) 1057 } 1058 1059 // Now args 1060 cmdargs = append(cmdargs, args...) 1061 cmd := gitNoLFS(cmdargs...) 1062 1063 // Assign all streams direct 1064 cmd.Stdout = os.Stdout 1065 cmd.Stderr = os.Stderr 1066 cmd.Stdin = os.Stdin 1067 1068 err := cmd.Start() 1069 if err != nil { 1070 return fmt.Errorf("failed to start git clone: %v", err) 1071 } 1072 1073 err = cmd.Wait() 1074 if err != nil { 1075 return fmt.Errorf("git clone failed: %v", err) 1076 } 1077 1078 return nil 1079} 1080 1081// Checkout performs an invocation of `git-checkout(1)` applying the given 1082// treeish, paths, and force option, if given. 1083// 1084// If any error was encountered, it will be returned immediately. Otherwise, the 1085// checkout has occurred successfully. 1086func Checkout(treeish string, paths []string, force bool) error { 1087 args := []string{"checkout"} 1088 if force { 1089 args = append(args, "--force") 1090 } 1091 1092 if len(treeish) > 0 { 1093 args = append(args, treeish) 1094 } 1095 1096 if len(paths) > 0 { 1097 args = append(args, append([]string{"--"}, paths...)...) 1098 } 1099 1100 _, err := gitNoLFSSimple(args...) 1101 return err 1102} 1103 1104// CachedRemoteRefs returns the list of branches & tags for a remote which are 1105// currently cached locally. No remote request is made to verify them. 1106func CachedRemoteRefs(remoteName string) ([]*Ref, error) { 1107 var ret []*Ref 1108 cmd := gitNoLFS("show-ref") 1109 1110 outp, err := cmd.StdoutPipe() 1111 if err != nil { 1112 return nil, fmt.Errorf("failed to call git show-ref: %v", err) 1113 } 1114 cmd.Start() 1115 scanner := bufio.NewScanner(outp) 1116 1117 refPrefix := fmt.Sprintf("refs/remotes/%v/", remoteName) 1118 for scanner.Scan() { 1119 if sha, name, ok := parseShowRefLine(refPrefix, scanner.Text()); ok { 1120 // Don't match head 1121 if name == "HEAD" { 1122 continue 1123 } 1124 ret = append(ret, &Ref{name, RefTypeRemoteBranch, sha}) 1125 } 1126 } 1127 return ret, cmd.Wait() 1128} 1129 1130func parseShowRefLine(refPrefix, line string) (sha, name string, ok bool) { 1131 // line format: <sha> <space> <ref> 1132 space := strings.IndexByte(line, ' ') 1133 if space < 0 { 1134 return "", "", false 1135 } 1136 ref := line[space+1:] 1137 if !strings.HasPrefix(ref, refPrefix) { 1138 return "", "", false 1139 } 1140 return line[:space], strings.TrimSpace(ref[len(refPrefix):]), true 1141} 1142 1143// Fetch performs a fetch with no arguments against the given remotes. 1144func Fetch(remotes ...string) error { 1145 if len(remotes) == 0 { 1146 return nil 1147 } 1148 1149 var args []string 1150 if len(remotes) > 1 { 1151 args = []string{"--multiple", "--"} 1152 } 1153 args = append(args, remotes...) 1154 1155 _, err := gitNoLFSSimple(append([]string{"fetch"}, args...)...) 1156 return err 1157} 1158 1159// RemoteRefs returns a list of branches & tags for a remote by actually 1160// accessing the remote via git ls-remote. 1161func RemoteRefs(remoteName string) ([]*Ref, error) { 1162 var ret []*Ref 1163 cmd := gitNoLFS("ls-remote", "--heads", "--tags", "-q", remoteName) 1164 1165 outp, err := cmd.StdoutPipe() 1166 if err != nil { 1167 return nil, fmt.Errorf("failed to call git ls-remote: %v", err) 1168 } 1169 cmd.Start() 1170 scanner := bufio.NewScanner(outp) 1171 1172 for scanner.Scan() { 1173 if sha, ns, name, ok := parseLsRemoteLine(scanner.Text()); ok { 1174 // Don't match head 1175 if name == "HEAD" { 1176 continue 1177 } 1178 1179 typ := RefTypeRemoteBranch 1180 if ns == "tags" { 1181 typ = RefTypeRemoteTag 1182 } 1183 ret = append(ret, &Ref{name, typ, sha}) 1184 } 1185 } 1186 return ret, cmd.Wait() 1187} 1188 1189func parseLsRemoteLine(line string) (sha, ns, name string, ok bool) { 1190 const headPrefix = "refs/heads/" 1191 const tagPrefix = "refs/tags/" 1192 1193 // line format: <sha> <tab> <ref> 1194 tab := strings.IndexByte(line, '\t') 1195 if tab < 0 { 1196 return "", "", "", false 1197 } 1198 ref := line[tab+1:] 1199 switch { 1200 case strings.HasPrefix(ref, headPrefix): 1201 ns = "heads" 1202 name = ref[len(headPrefix):] 1203 case strings.HasPrefix(ref, tagPrefix): 1204 ns = "tags" 1205 name = ref[len(tagPrefix):] 1206 default: 1207 return "", "", "", false 1208 } 1209 return line[:tab], ns, strings.TrimSpace(name), true 1210} 1211 1212// AllRefs returns a slice of all references in a Git repository in the current 1213// working directory, or an error if those references could not be loaded. 1214func AllRefs() ([]*Ref, error) { 1215 return AllRefsIn("") 1216} 1217 1218// AllRefs returns a slice of all references in a Git repository located in a 1219// the given working directory "wd", or an error if those references could not 1220// be loaded. 1221func AllRefsIn(wd string) ([]*Ref, error) { 1222 cmd := gitNoLFS( 1223 "for-each-ref", "--format=%(objectname)%00%(refname)") 1224 cmd.Dir = wd 1225 1226 outp, err := cmd.StdoutPipe() 1227 if err != nil { 1228 return nil, lfserrors.Wrap(err, "cannot open pipe") 1229 } 1230 cmd.Start() 1231 1232 refs := make([]*Ref, 0) 1233 1234 scanner := bufio.NewScanner(outp) 1235 for scanner.Scan() { 1236 parts := strings.SplitN(scanner.Text(), "\x00", 2) 1237 if len(parts) != 2 { 1238 return nil, lfserrors.Errorf( 1239 "git: invalid for-each-ref line: %q", scanner.Text()) 1240 } 1241 1242 sha := parts[0] 1243 typ, name := ParseRefToTypeAndName(parts[1]) 1244 1245 refs = append(refs, &Ref{ 1246 Name: name, 1247 Type: typ, 1248 Sha: sha, 1249 }) 1250 } 1251 1252 if err := scanner.Err(); err != nil { 1253 return nil, err 1254 } 1255 1256 return refs, nil 1257} 1258 1259// GetTrackedFiles returns a list of files which are tracked in Git which match 1260// the pattern specified (standard wildcard form) 1261// Both pattern and the results are relative to the current working directory, not 1262// the root of the repository 1263func GetTrackedFiles(pattern string) ([]string, error) { 1264 safePattern := sanitizePattern(pattern) 1265 rootWildcard := len(safePattern) < len(pattern) && strings.ContainsRune(safePattern, '*') 1266 1267 var ret []string 1268 cmd := gitNoLFS( 1269 "-c", "core.quotepath=false", // handle special chars in filenames 1270 "ls-files", 1271 "--cached", // include things which are staged but not committed right now 1272 "--", // no ambiguous patterns 1273 safePattern) 1274 1275 outp, err := cmd.StdoutPipe() 1276 if err != nil { 1277 return nil, fmt.Errorf("failed to call git ls-files: %v", err) 1278 } 1279 cmd.Start() 1280 scanner := bufio.NewScanner(outp) 1281 for scanner.Scan() { 1282 line := scanner.Text() 1283 1284 // If the given pattern is a root wildcard, skip all files which 1285 // are not direct descendants of the repository's root. 1286 // 1287 // This matches the behavior of how .gitattributes performs 1288 // filename matches. 1289 if rootWildcard && filepath.Dir(line) != "." { 1290 continue 1291 } 1292 1293 ret = append(ret, strings.TrimSpace(line)) 1294 } 1295 return ret, cmd.Wait() 1296} 1297 1298func sanitizePattern(pattern string) string { 1299 if strings.HasPrefix(pattern, "/") { 1300 return pattern[1:] 1301 } 1302 1303 return pattern 1304} 1305 1306// GetFilesChanged returns a list of files which were changed, either between 2 1307// commits, or at a single commit if you only supply one argument and a blank 1308// string for the other 1309func GetFilesChanged(from, to string) ([]string, error) { 1310 var files []string 1311 args := []string{ 1312 "-c", "core.quotepath=false", // handle special chars in filenames 1313 "diff-tree", 1314 "--no-commit-id", 1315 "--name-only", 1316 "-r", 1317 } 1318 1319 if len(from) > 0 { 1320 args = append(args, from) 1321 } 1322 if len(to) > 0 { 1323 args = append(args, to) 1324 } 1325 args = append(args, "--") // no ambiguous patterns 1326 1327 cmd := gitNoLFS(args...) 1328 outp, err := cmd.StdoutPipe() 1329 if err != nil { 1330 return nil, fmt.Errorf("failed to call git diff: %v", err) 1331 } 1332 if err := cmd.Start(); err != nil { 1333 return nil, fmt.Errorf("failed to start git diff: %v", err) 1334 } 1335 scanner := bufio.NewScanner(outp) 1336 for scanner.Scan() { 1337 files = append(files, strings.TrimSpace(scanner.Text())) 1338 } 1339 if err := cmd.Wait(); err != nil { 1340 return nil, fmt.Errorf("git diff failed: %v", err) 1341 } 1342 1343 return files, err 1344} 1345 1346// IsFileModified returns whether the filepath specified is modified according 1347// to `git status`. A file is modified if it has uncommitted changes in the 1348// working copy or the index. This includes being untracked. 1349func IsFileModified(filepath string) (bool, error) { 1350 1351 args := []string{ 1352 "-c", "core.quotepath=false", // handle special chars in filenames 1353 "status", 1354 "--porcelain", 1355 "--", // separator in case filename ambiguous 1356 filepath, 1357 } 1358 cmd := git(args...) 1359 outp, err := cmd.StdoutPipe() 1360 if err != nil { 1361 return false, lfserrors.Wrap(err, "Failed to call git status") 1362 } 1363 if err := cmd.Start(); err != nil { 1364 return false, lfserrors.Wrap(err, "Failed to start git status") 1365 } 1366 matched := false 1367 for scanner := bufio.NewScanner(outp); scanner.Scan(); { 1368 line := scanner.Text() 1369 // Porcelain format is "<I><W> <filename>" 1370 // Where <I> = index status, <W> = working copy status 1371 if len(line) > 3 { 1372 // Double-check even though should be only match 1373 if strings.TrimSpace(line[3:]) == filepath { 1374 matched = true 1375 // keep consuming output to exit cleanly 1376 // will typically fall straight through anyway due to 1 line output 1377 } 1378 } 1379 } 1380 if err := cmd.Wait(); err != nil { 1381 return false, lfserrors.Wrap(err, "Git status failed") 1382 } 1383 1384 return matched, nil 1385} 1386 1387// IsWorkingCopyDirty returns true if and only if the working copy in which the 1388// command was executed is dirty as compared to the index. 1389// 1390// If the status of the working copy could not be determined, an error will be 1391// returned instead. 1392func IsWorkingCopyDirty() (bool, error) { 1393 bare, err := IsBare() 1394 if bare || err != nil { 1395 return false, err 1396 } 1397 1398 out, err := gitSimple("status", "--porcelain") 1399 if err != nil { 1400 return false, err 1401 } 1402 return len(out) != 0, nil 1403} 1404 1405func ObjectDatabase(osEnv, gitEnv Environment, gitdir, tempdir string) (*gitobj.ObjectDatabase, error) { 1406 var options []gitobj.Option 1407 objdir, ok := osEnv.Get("GIT_OBJECT_DIRECTORY") 1408 if !ok { 1409 objdir = filepath.Join(gitdir, "objects") 1410 } 1411 alternates, _ := osEnv.Get("GIT_ALTERNATE_OBJECT_DIRECTORIES") 1412 if alternates != "" { 1413 options = append(options, gitobj.Alternates(alternates)) 1414 } 1415 hashAlgo, _ := gitEnv.Get("extensions.objectformat") 1416 if hashAlgo != "" { 1417 options = append(options, gitobj.ObjectFormat(gitobj.ObjectFormatAlgorithm(hashAlgo))) 1418 } 1419 odb, err := gitobj.FromFilesystem(objdir, tempdir, options...) 1420 if err != nil { 1421 return nil, err 1422 } 1423 if odb.Hasher() == nil { 1424 return nil, fmt.Errorf("unsupported repository hash algorithm %q", hashAlgo) 1425 } 1426 return odb, nil 1427} 1428