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