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