1// Copyright 2018 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package codehost 6 7import ( 8 "bytes" 9 "errors" 10 "fmt" 11 exec "internal/execabs" 12 "io" 13 "io/fs" 14 "net/url" 15 "os" 16 "path/filepath" 17 "sort" 18 "strconv" 19 "strings" 20 "sync" 21 "time" 22 23 "cmd/go/internal/lockedfile" 24 "cmd/go/internal/par" 25 "cmd/go/internal/web" 26 27 "golang.org/x/mod/semver" 28) 29 30// LocalGitRepo is like Repo but accepts both Git remote references 31// and paths to repositories on the local file system. 32func LocalGitRepo(remote string) (Repo, error) { 33 return newGitRepoCached(remote, true) 34} 35 36// A notExistError wraps another error to retain its original text 37// but makes it opaquely equivalent to fs.ErrNotExist. 38type notExistError struct { 39 err error 40} 41 42func (e notExistError) Error() string { return e.err.Error() } 43func (notExistError) Is(err error) bool { return err == fs.ErrNotExist } 44 45const gitWorkDirType = "git3" 46 47var gitRepoCache par.Cache 48 49func newGitRepoCached(remote string, localOK bool) (Repo, error) { 50 type key struct { 51 remote string 52 localOK bool 53 } 54 type cached struct { 55 repo Repo 56 err error 57 } 58 59 c := gitRepoCache.Do(key{remote, localOK}, func() interface{} { 60 repo, err := newGitRepo(remote, localOK) 61 return cached{repo, err} 62 }).(cached) 63 64 return c.repo, c.err 65} 66 67func newGitRepo(remote string, localOK bool) (Repo, error) { 68 r := &gitRepo{remote: remote} 69 if strings.Contains(remote, "://") { 70 // This is a remote path. 71 var err error 72 r.dir, r.mu.Path, err = WorkDir(gitWorkDirType, r.remote) 73 if err != nil { 74 return nil, err 75 } 76 77 unlock, err := r.mu.Lock() 78 if err != nil { 79 return nil, err 80 } 81 defer unlock() 82 83 if _, err := os.Stat(filepath.Join(r.dir, "objects")); err != nil { 84 if _, err := Run(r.dir, "git", "init", "--bare"); err != nil { 85 os.RemoveAll(r.dir) 86 return nil, err 87 } 88 // We could just say git fetch https://whatever later, 89 // but this lets us say git fetch origin instead, which 90 // is a little nicer. More importantly, using a named remote 91 // avoids a problem with Git LFS. See golang.org/issue/25605. 92 if _, err := Run(r.dir, "git", "remote", "add", "origin", "--", r.remote); err != nil { 93 os.RemoveAll(r.dir) 94 return nil, err 95 } 96 } 97 r.remoteURL = r.remote 98 r.remote = "origin" 99 } else { 100 // Local path. 101 // Disallow colon (not in ://) because sometimes 102 // that's rcp-style host:path syntax and sometimes it's not (c:\work). 103 // The go command has always insisted on URL syntax for ssh. 104 if strings.Contains(remote, ":") { 105 return nil, fmt.Errorf("git remote cannot use host:path syntax") 106 } 107 if !localOK { 108 return nil, fmt.Errorf("git remote must not be local directory") 109 } 110 r.local = true 111 info, err := os.Stat(remote) 112 if err != nil { 113 return nil, err 114 } 115 if !info.IsDir() { 116 return nil, fmt.Errorf("%s exists but is not a directory", remote) 117 } 118 r.dir = remote 119 r.mu.Path = r.dir + ".lock" 120 } 121 return r, nil 122} 123 124type gitRepo struct { 125 remote, remoteURL string 126 local bool 127 dir string 128 129 mu lockedfile.Mutex // protects fetchLevel and git repo state 130 131 fetchLevel int 132 133 statCache par.Cache 134 135 refsOnce sync.Once 136 // refs maps branch and tag refs (e.g., "HEAD", "refs/heads/master") 137 // to commits (e.g., "37ffd2e798afde829a34e8955b716ab730b2a6d6") 138 refs map[string]string 139 refsErr error 140 141 localTagsOnce sync.Once 142 localTags map[string]bool 143} 144 145const ( 146 // How much have we fetched into the git repo (in this process)? 147 fetchNone = iota // nothing yet 148 fetchSome // shallow fetches of individual hashes 149 fetchAll // "fetch -t origin": get all remote branches and tags 150) 151 152// loadLocalTags loads tag references from the local git cache 153// into the map r.localTags. 154// Should only be called as r.localTagsOnce.Do(r.loadLocalTags). 155func (r *gitRepo) loadLocalTags() { 156 // The git protocol sends all known refs and ls-remote filters them on the client side, 157 // so we might as well record both heads and tags in one shot. 158 // Most of the time we only care about tags but sometimes we care about heads too. 159 out, err := Run(r.dir, "git", "tag", "-l") 160 if err != nil { 161 return 162 } 163 164 r.localTags = make(map[string]bool) 165 for _, line := range strings.Split(string(out), "\n") { 166 if line != "" { 167 r.localTags[line] = true 168 } 169 } 170} 171 172// loadRefs loads heads and tags references from the remote into the map r.refs. 173// Should only be called as r.refsOnce.Do(r.loadRefs). 174func (r *gitRepo) loadRefs() { 175 // The git protocol sends all known refs and ls-remote filters them on the client side, 176 // so we might as well record both heads and tags in one shot. 177 // Most of the time we only care about tags but sometimes we care about heads too. 178 out, gitErr := Run(r.dir, "git", "ls-remote", "-q", r.remote) 179 if gitErr != nil { 180 if rerr, ok := gitErr.(*RunError); ok { 181 if bytes.Contains(rerr.Stderr, []byte("fatal: could not read Username")) { 182 rerr.HelpText = "Confirm the import path was entered correctly.\nIf this is a private repository, see https://golang.org/doc/faq#git_https for additional information." 183 } 184 } 185 186 // If the remote URL doesn't exist at all, ideally we should treat the whole 187 // repository as nonexistent by wrapping the error in a notExistError. 188 // For HTTP and HTTPS, that's easy to detect: we'll try to fetch the URL 189 // ourselves and see what code it serves. 190 if u, err := url.Parse(r.remoteURL); err == nil && (u.Scheme == "http" || u.Scheme == "https") { 191 if _, err := web.GetBytes(u); errors.Is(err, fs.ErrNotExist) { 192 gitErr = notExistError{gitErr} 193 } 194 } 195 196 r.refsErr = gitErr 197 return 198 } 199 200 r.refs = make(map[string]string) 201 for _, line := range strings.Split(string(out), "\n") { 202 f := strings.Fields(line) 203 if len(f) != 2 { 204 continue 205 } 206 if f[1] == "HEAD" || strings.HasPrefix(f[1], "refs/heads/") || strings.HasPrefix(f[1], "refs/tags/") { 207 r.refs[f[1]] = f[0] 208 } 209 } 210 for ref, hash := range r.refs { 211 if strings.HasSuffix(ref, "^{}") { // record unwrapped annotated tag as value of tag 212 r.refs[strings.TrimSuffix(ref, "^{}")] = hash 213 delete(r.refs, ref) 214 } 215 } 216} 217 218func (r *gitRepo) Tags(prefix string) ([]string, error) { 219 r.refsOnce.Do(r.loadRefs) 220 if r.refsErr != nil { 221 return nil, r.refsErr 222 } 223 224 tags := []string{} 225 for ref := range r.refs { 226 if !strings.HasPrefix(ref, "refs/tags/") { 227 continue 228 } 229 tag := ref[len("refs/tags/"):] 230 if !strings.HasPrefix(tag, prefix) { 231 continue 232 } 233 tags = append(tags, tag) 234 } 235 sort.Strings(tags) 236 return tags, nil 237} 238 239func (r *gitRepo) Latest() (*RevInfo, error) { 240 r.refsOnce.Do(r.loadRefs) 241 if r.refsErr != nil { 242 return nil, r.refsErr 243 } 244 if r.refs["HEAD"] == "" { 245 return nil, ErrNoCommits 246 } 247 return r.Stat(r.refs["HEAD"]) 248} 249 250// findRef finds some ref name for the given hash, 251// for use when the server requires giving a ref instead of a hash. 252// There may be multiple ref names for a given hash, 253// in which case this returns some name - it doesn't matter which. 254func (r *gitRepo) findRef(hash string) (ref string, ok bool) { 255 r.refsOnce.Do(r.loadRefs) 256 for ref, h := range r.refs { 257 if h == hash { 258 return ref, true 259 } 260 } 261 return "", false 262} 263 264// minHashDigits is the minimum number of digits to require 265// before accepting a hex digit sequence as potentially identifying 266// a specific commit in a git repo. (Of course, users can always 267// specify more digits, and many will paste in all 40 digits, 268// but many of git's commands default to printing short hashes 269// as 7 digits.) 270const minHashDigits = 7 271 272// stat stats the given rev in the local repository, 273// or else it fetches more info from the remote repository and tries again. 274func (r *gitRepo) stat(rev string) (*RevInfo, error) { 275 if r.local { 276 return r.statLocal(rev, rev) 277 } 278 279 // Fast path: maybe rev is a hash we already have locally. 280 didStatLocal := false 281 if len(rev) >= minHashDigits && len(rev) <= 40 && AllHex(rev) { 282 if info, err := r.statLocal(rev, rev); err == nil { 283 return info, nil 284 } 285 didStatLocal = true 286 } 287 288 // Maybe rev is a tag we already have locally. 289 // (Note that we're excluding branches, which can be stale.) 290 r.localTagsOnce.Do(r.loadLocalTags) 291 if r.localTags[rev] { 292 return r.statLocal(rev, "refs/tags/"+rev) 293 } 294 295 // Maybe rev is the name of a tag or branch on the remote server. 296 // Or maybe it's the prefix of a hash of a named ref. 297 // Try to resolve to both a ref (git name) and full (40-hex-digit) commit hash. 298 r.refsOnce.Do(r.loadRefs) 299 var ref, hash string 300 if r.refs["refs/tags/"+rev] != "" { 301 ref = "refs/tags/" + rev 302 hash = r.refs[ref] 303 // Keep rev as is: tags are assumed not to change meaning. 304 } else if r.refs["refs/heads/"+rev] != "" { 305 ref = "refs/heads/" + rev 306 hash = r.refs[ref] 307 rev = hash // Replace rev, because meaning of refs/heads/foo can change. 308 } else if rev == "HEAD" && r.refs["HEAD"] != "" { 309 ref = "HEAD" 310 hash = r.refs[ref] 311 rev = hash // Replace rev, because meaning of HEAD can change. 312 } else if len(rev) >= minHashDigits && len(rev) <= 40 && AllHex(rev) { 313 // At the least, we have a hash prefix we can look up after the fetch below. 314 // Maybe we can map it to a full hash using the known refs. 315 prefix := rev 316 // Check whether rev is prefix of known ref hash. 317 for k, h := range r.refs { 318 if strings.HasPrefix(h, prefix) { 319 if hash != "" && hash != h { 320 // Hash is an ambiguous hash prefix. 321 // More information will not change that. 322 return nil, fmt.Errorf("ambiguous revision %s", rev) 323 } 324 if ref == "" || ref > k { // Break ties deterministically when multiple refs point at same hash. 325 ref = k 326 } 327 rev = h 328 hash = h 329 } 330 } 331 if hash == "" && len(rev) == 40 { // Didn't find a ref, but rev is a full hash. 332 hash = rev 333 } 334 } else { 335 return nil, &UnknownRevisionError{Rev: rev} 336 } 337 338 // Protect r.fetchLevel and the "fetch more and more" sequence. 339 unlock, err := r.mu.Lock() 340 if err != nil { 341 return nil, err 342 } 343 defer unlock() 344 345 // Perhaps r.localTags did not have the ref when we loaded local tags, 346 // but we've since done fetches that pulled down the hash we need 347 // (or already have the hash we need, just without its tag). 348 // Either way, try a local stat before falling back to network I/O. 349 if !didStatLocal { 350 if info, err := r.statLocal(rev, hash); err == nil { 351 if strings.HasPrefix(ref, "refs/tags/") { 352 // Make sure tag exists, so it will be in localTags next time the go command is run. 353 Run(r.dir, "git", "tag", strings.TrimPrefix(ref, "refs/tags/"), hash) 354 } 355 return info, nil 356 } 357 } 358 359 // If we know a specific commit we need and its ref, fetch it. 360 // We do NOT fetch arbitrary hashes (when we don't know the ref) 361 // because we want to avoid ever importing a commit that isn't 362 // reachable from refs/tags/* or refs/heads/* or HEAD. 363 // Both Gerrit and GitHub expose every CL/PR as a named ref, 364 // and we don't want those commits masquerading as being real 365 // pseudo-versions in the main repo. 366 if r.fetchLevel <= fetchSome && ref != "" && hash != "" && !r.local { 367 r.fetchLevel = fetchSome 368 var refspec string 369 if ref != "" && ref != "HEAD" { 370 // If we do know the ref name, save the mapping locally 371 // so that (if it is a tag) it can show up in localTags 372 // on a future call. Also, some servers refuse to allow 373 // full hashes in ref specs, so prefer a ref name if known. 374 refspec = ref + ":" + ref 375 } else { 376 // Fetch the hash but give it a local name (refs/dummy), 377 // because that triggers the fetch behavior of creating any 378 // other known remote tags for the hash. We never use 379 // refs/dummy (it's not refs/tags/dummy) and it will be 380 // overwritten in the next command, and that's fine. 381 ref = hash 382 refspec = hash + ":refs/dummy" 383 } 384 _, err := Run(r.dir, "git", "fetch", "-f", "--depth=1", r.remote, refspec) 385 if err == nil { 386 return r.statLocal(rev, ref) 387 } 388 // Don't try to be smart about parsing the error. 389 // It's too complex and varies too much by git version. 390 // No matter what went wrong, fall back to a complete fetch. 391 } 392 393 // Last resort. 394 // Fetch all heads and tags and hope the hash we want is in the history. 395 if err := r.fetchRefsLocked(); err != nil { 396 return nil, err 397 } 398 399 return r.statLocal(rev, rev) 400} 401 402// fetchRefsLocked fetches all heads and tags from the origin, along with the 403// ancestors of those commits. 404// 405// We only fetch heads and tags, not arbitrary other commits: we don't want to 406// pull in off-branch commits (such as rejected GitHub pull requests) that the 407// server may be willing to provide. (See the comments within the stat method 408// for more detail.) 409// 410// fetchRefsLocked requires that r.mu remain locked for the duration of the call. 411func (r *gitRepo) fetchRefsLocked() error { 412 if r.fetchLevel < fetchAll { 413 // NOTE: To work around a bug affecting Git clients up to at least 2.23.0 414 // (2019-08-16), we must first expand the set of local refs, and only then 415 // unshallow the repository as a separate fetch operation. (See 416 // golang.org/issue/34266 and 417 // https://github.com/git/git/blob/4c86140027f4a0d2caaa3ab4bd8bfc5ce3c11c8a/transport.c#L1303-L1309.) 418 419 if _, err := Run(r.dir, "git", "fetch", "-f", r.remote, "refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"); err != nil { 420 return err 421 } 422 423 if _, err := os.Stat(filepath.Join(r.dir, "shallow")); err == nil { 424 if _, err := Run(r.dir, "git", "fetch", "--unshallow", "-f", r.remote); err != nil { 425 return err 426 } 427 } 428 429 r.fetchLevel = fetchAll 430 } 431 return nil 432} 433 434// statLocal returns a RevInfo describing rev in the local git repository. 435// It uses version as info.Version. 436func (r *gitRepo) statLocal(version, rev string) (*RevInfo, error) { 437 out, err := Run(r.dir, "git", "-c", "log.showsignature=false", "log", "-n1", "--format=format:%H %ct %D", rev, "--") 438 if err != nil { 439 return nil, &UnknownRevisionError{Rev: rev} 440 } 441 f := strings.Fields(string(out)) 442 if len(f) < 2 { 443 return nil, fmt.Errorf("unexpected response from git log: %q", out) 444 } 445 hash := f[0] 446 if strings.HasPrefix(hash, version) { 447 version = hash // extend to full hash 448 } 449 t, err := strconv.ParseInt(f[1], 10, 64) 450 if err != nil { 451 return nil, fmt.Errorf("invalid time from git log: %q", out) 452 } 453 454 info := &RevInfo{ 455 Name: hash, 456 Short: ShortenSHA1(hash), 457 Time: time.Unix(t, 0).UTC(), 458 Version: hash, 459 } 460 461 // Add tags. Output looks like: 462 // ede458df7cd0fdca520df19a33158086a8a68e81 1523994202 HEAD -> master, tag: v1.2.4-annotated, tag: v1.2.3, origin/master, origin/HEAD 463 for i := 2; i < len(f); i++ { 464 if f[i] == "tag:" { 465 i++ 466 if i < len(f) { 467 info.Tags = append(info.Tags, strings.TrimSuffix(f[i], ",")) 468 } 469 } 470 } 471 sort.Strings(info.Tags) 472 473 // Used hash as info.Version above. 474 // Use caller's suggested version if it appears in the tag list 475 // (filters out branch names, HEAD). 476 for _, tag := range info.Tags { 477 if version == tag { 478 info.Version = version 479 } 480 } 481 482 return info, nil 483} 484 485func (r *gitRepo) Stat(rev string) (*RevInfo, error) { 486 if rev == "latest" { 487 return r.Latest() 488 } 489 type cached struct { 490 info *RevInfo 491 err error 492 } 493 c := r.statCache.Do(rev, func() interface{} { 494 info, err := r.stat(rev) 495 return cached{info, err} 496 }).(cached) 497 return c.info, c.err 498} 499 500func (r *gitRepo) ReadFile(rev, file string, maxSize int64) ([]byte, error) { 501 // TODO: Could use git cat-file --batch. 502 info, err := r.Stat(rev) // download rev into local git repo 503 if err != nil { 504 return nil, err 505 } 506 out, err := Run(r.dir, "git", "cat-file", "blob", info.Name+":"+file) 507 if err != nil { 508 return nil, fs.ErrNotExist 509 } 510 return out, nil 511} 512 513func (r *gitRepo) ReadFileRevs(revs []string, file string, maxSize int64) (map[string]*FileRev, error) { 514 // Create space to hold results. 515 files := make(map[string]*FileRev) 516 for _, rev := range revs { 517 f := &FileRev{Rev: rev} 518 files[rev] = f 519 } 520 521 // Collect locally-known revs. 522 need, err := r.readFileRevs(revs, file, files) 523 if err != nil { 524 return nil, err 525 } 526 if len(need) == 0 { 527 return files, nil 528 } 529 530 // Build list of known remote refs that might help. 531 var redo []string 532 r.refsOnce.Do(r.loadRefs) 533 if r.refsErr != nil { 534 return nil, r.refsErr 535 } 536 for _, tag := range need { 537 if r.refs["refs/tags/"+tag] != "" { 538 redo = append(redo, tag) 539 } 540 } 541 if len(redo) == 0 { 542 return files, nil 543 } 544 545 // Protect r.fetchLevel and the "fetch more and more" sequence. 546 // See stat method above. 547 unlock, err := r.mu.Lock() 548 if err != nil { 549 return nil, err 550 } 551 defer unlock() 552 553 if err := r.fetchRefsLocked(); err != nil { 554 return nil, err 555 } 556 557 if _, err := r.readFileRevs(redo, file, files); err != nil { 558 return nil, err 559 } 560 561 return files, nil 562} 563 564func (r *gitRepo) readFileRevs(tags []string, file string, fileMap map[string]*FileRev) (missing []string, err error) { 565 var stdin bytes.Buffer 566 for _, tag := range tags { 567 fmt.Fprintf(&stdin, "refs/tags/%s\n", tag) 568 fmt.Fprintf(&stdin, "refs/tags/%s:%s\n", tag, file) 569 } 570 571 data, err := RunWithStdin(r.dir, &stdin, "git", "cat-file", "--batch") 572 if err != nil { 573 return nil, err 574 } 575 576 next := func() (typ string, body []byte, ok bool) { 577 var line string 578 i := bytes.IndexByte(data, '\n') 579 if i < 0 { 580 return "", nil, false 581 } 582 line, data = string(bytes.TrimSpace(data[:i])), data[i+1:] 583 if strings.HasSuffix(line, " missing") { 584 return "missing", nil, true 585 } 586 f := strings.Fields(line) 587 if len(f) != 3 { 588 return "", nil, false 589 } 590 n, err := strconv.Atoi(f[2]) 591 if err != nil || n > len(data) { 592 return "", nil, false 593 } 594 body, data = data[:n], data[n:] 595 if len(data) > 0 && data[0] == '\r' { 596 data = data[1:] 597 } 598 if len(data) > 0 && data[0] == '\n' { 599 data = data[1:] 600 } 601 return f[1], body, true 602 } 603 604 badGit := func() ([]string, error) { 605 return nil, fmt.Errorf("malformed output from git cat-file --batch") 606 } 607 608 for _, tag := range tags { 609 commitType, _, ok := next() 610 if !ok { 611 return badGit() 612 } 613 fileType, fileData, ok := next() 614 if !ok { 615 return badGit() 616 } 617 f := fileMap[tag] 618 f.Data = nil 619 f.Err = nil 620 switch commitType { 621 default: 622 f.Err = fmt.Errorf("unexpected non-commit type %q for rev %s", commitType, tag) 623 624 case "missing": 625 // Note: f.Err must not satisfy os.IsNotExist. That's reserved for the file not existing in a valid commit. 626 f.Err = fmt.Errorf("no such rev %s", tag) 627 missing = append(missing, tag) 628 629 case "tag", "commit": 630 switch fileType { 631 default: 632 f.Err = &fs.PathError{Path: tag + ":" + file, Op: "read", Err: fmt.Errorf("unexpected non-blob type %q", fileType)} 633 case "missing": 634 f.Err = &fs.PathError{Path: tag + ":" + file, Op: "read", Err: fs.ErrNotExist} 635 case "blob": 636 f.Data = fileData 637 } 638 } 639 } 640 if len(bytes.TrimSpace(data)) != 0 { 641 return badGit() 642 } 643 644 return missing, nil 645} 646 647func (r *gitRepo) RecentTag(rev, prefix string, allowed func(string) bool) (tag string, err error) { 648 info, err := r.Stat(rev) 649 if err != nil { 650 return "", err 651 } 652 rev = info.Name // expand hash prefixes 653 654 // describe sets tag and err using 'git for-each-ref' and reports whether the 655 // result is definitive. 656 describe := func() (definitive bool) { 657 var out []byte 658 out, err = Run(r.dir, "git", "for-each-ref", "--format", "%(refname)", "refs/tags", "--merged", rev) 659 if err != nil { 660 return true 661 } 662 663 // prefixed tags aren't valid semver tags so compare without prefix, but only tags with correct prefix 664 var highest string 665 for _, line := range strings.Split(string(out), "\n") { 666 line = strings.TrimSpace(line) 667 // git do support lstrip in for-each-ref format, but it was added in v2.13.0. Stripping here 668 // instead gives support for git v2.7.0. 669 if !strings.HasPrefix(line, "refs/tags/") { 670 continue 671 } 672 line = line[len("refs/tags/"):] 673 674 if !strings.HasPrefix(line, prefix) { 675 continue 676 } 677 678 semtag := line[len(prefix):] 679 // Consider only tags that are valid and complete (not just major.minor prefixes). 680 // NOTE: Do not replace the call to semver.Compare with semver.Max. 681 // We want to return the actual tag, not a canonicalized version of it, 682 // and semver.Max currently canonicalizes (see golang.org/issue/32700). 683 if c := semver.Canonical(semtag); c == "" || !strings.HasPrefix(semtag, c) || !allowed(semtag) { 684 continue 685 } 686 if semver.Compare(semtag, highest) > 0 { 687 highest = semtag 688 } 689 } 690 691 if highest != "" { 692 tag = prefix + highest 693 } 694 695 return tag != "" && !AllHex(tag) 696 } 697 698 if describe() { 699 return tag, err 700 } 701 702 // Git didn't find a version tag preceding the requested rev. 703 // See whether any plausible tag exists. 704 tags, err := r.Tags(prefix + "v") 705 if err != nil { 706 return "", err 707 } 708 if len(tags) == 0 { 709 return "", nil 710 } 711 712 // There are plausible tags, but we don't know if rev is a descendent of any of them. 713 // Fetch the history to find out. 714 715 unlock, err := r.mu.Lock() 716 if err != nil { 717 return "", err 718 } 719 defer unlock() 720 721 if err := r.fetchRefsLocked(); err != nil { 722 return "", err 723 } 724 725 // If we've reached this point, we have all of the commits that are reachable 726 // from all heads and tags. 727 // 728 // The only refs we should be missing are those that are no longer reachable 729 // (or never were reachable) from any branch or tag, including the master 730 // branch, and we don't want to resolve them anyway (they're probably 731 // unreachable for a reason). 732 // 733 // Try one last time in case some other goroutine fetched rev while we were 734 // waiting on the lock. 735 describe() 736 return tag, err 737} 738 739func (r *gitRepo) DescendsFrom(rev, tag string) (bool, error) { 740 // The "--is-ancestor" flag was added to "git merge-base" in version 1.8.0, so 741 // this won't work with Git 1.7.1. According to golang.org/issue/28550, cmd/go 742 // already doesn't work with Git 1.7.1, so at least it's not a regression. 743 // 744 // git merge-base --is-ancestor exits with status 0 if rev is an ancestor, or 745 // 1 if not. 746 _, err := Run(r.dir, "git", "merge-base", "--is-ancestor", "--", tag, rev) 747 748 // Git reports "is an ancestor" with exit code 0 and "not an ancestor" with 749 // exit code 1. 750 // Unfortunately, if we've already fetched rev with a shallow history, git 751 // merge-base has been observed to report a false-negative, so don't stop yet 752 // even if the exit code is 1! 753 if err == nil { 754 return true, nil 755 } 756 757 // See whether the tag and rev even exist. 758 tags, err := r.Tags(tag) 759 if err != nil { 760 return false, err 761 } 762 if len(tags) == 0 { 763 return false, nil 764 } 765 766 // NOTE: r.stat is very careful not to fetch commits that we shouldn't know 767 // about, like rejected GitHub pull requests, so don't try to short-circuit 768 // that here. 769 if _, err = r.stat(rev); err != nil { 770 return false, err 771 } 772 773 // Now fetch history so that git can search for a path. 774 unlock, err := r.mu.Lock() 775 if err != nil { 776 return false, err 777 } 778 defer unlock() 779 780 if r.fetchLevel < fetchAll { 781 // Fetch the complete history for all refs and heads. It would be more 782 // efficient to only fetch the history from rev to tag, but that's much more 783 // complicated, and any kind of shallow fetch is fairly likely to trigger 784 // bugs in JGit servers and/or the go command anyway. 785 if err := r.fetchRefsLocked(); err != nil { 786 return false, err 787 } 788 } 789 790 _, err = Run(r.dir, "git", "merge-base", "--is-ancestor", "--", tag, rev) 791 if err == nil { 792 return true, nil 793 } 794 if ee, ok := err.(*RunError).Err.(*exec.ExitError); ok && ee.ExitCode() == 1 { 795 return false, nil 796 } 797 return false, err 798} 799 800func (r *gitRepo) ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, err error) { 801 // TODO: Use maxSize or drop it. 802 args := []string{} 803 if subdir != "" { 804 args = append(args, "--", subdir) 805 } 806 info, err := r.Stat(rev) // download rev into local git repo 807 if err != nil { 808 return nil, err 809 } 810 811 unlock, err := r.mu.Lock() 812 if err != nil { 813 return nil, err 814 } 815 defer unlock() 816 817 if err := ensureGitAttributes(r.dir); err != nil { 818 return nil, err 819 } 820 821 // Incredibly, git produces different archives depending on whether 822 // it is running on a Windows system or not, in an attempt to normalize 823 // text file line endings. Setting -c core.autocrlf=input means only 824 // translate files on the way into the repo, not on the way out (archive). 825 // The -c core.eol=lf should be unnecessary but set it anyway. 826 archive, err := Run(r.dir, "git", "-c", "core.autocrlf=input", "-c", "core.eol=lf", "archive", "--format=zip", "--prefix=prefix/", info.Name, args) 827 if err != nil { 828 if bytes.Contains(err.(*RunError).Stderr, []byte("did not match any files")) { 829 return nil, fs.ErrNotExist 830 } 831 return nil, err 832 } 833 834 return io.NopCloser(bytes.NewReader(archive)), nil 835} 836 837// ensureGitAttributes makes sure export-subst and export-ignore features are 838// disabled for this repo. This is intended to be run prior to running git 839// archive so that zip files are generated that produce consistent ziphashes 840// for a given revision, independent of variables such as git version and the 841// size of the repo. 842// 843// See: https://github.com/golang/go/issues/27153 844func ensureGitAttributes(repoDir string) (err error) { 845 const attr = "\n* -export-subst -export-ignore\n" 846 847 d := repoDir + "/info" 848 p := d + "/attributes" 849 850 if err := os.MkdirAll(d, 0755); err != nil { 851 return err 852 } 853 854 f, err := os.OpenFile(p, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666) 855 if err != nil { 856 return err 857 } 858 defer func() { 859 closeErr := f.Close() 860 if closeErr != nil { 861 err = closeErr 862 } 863 }() 864 865 b, err := io.ReadAll(f) 866 if err != nil { 867 return err 868 } 869 if !bytes.HasSuffix(b, []byte(attr)) { 870 _, err := f.WriteString(attr) 871 return err 872 } 873 874 return nil 875} 876