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