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