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