github.com/knieriem/gointernal@v0.2.0-pre2/cmd/go/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 "errors" 10 "fmt" 11 exec "github.com/knieriem/gointernal/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 "github.com/knieriem/gointernal/cmd/go/internal/lockedfile" 24 "github.com/knieriem/gointernal/cmd/go/internal/par" 25 "github.com/knieriem/gointernal/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. 32 func 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. 38 type notExistError struct { 39 err error 40 } 41 42 func (e notExistError) Error() string { return e.err.Error() } 43 func (notExistError) Is(err error) bool { return err == fs.ErrNotExist } 44 45 const gitWorkDirType = "git3" 46 47 var gitRepoCache par.Cache 48 49 func 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 67 func 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 124 type 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 145 const ( 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). 155 func (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 // The result is cached in memory. 174 func (r *gitRepo) loadRefs() (map[string]string, error) { 175 r.refsOnce.Do(func() { 176 // The git protocol sends all known refs and ls-remote filters them on the client side, 177 // so we might as well record both heads and tags in one shot. 178 // Most of the time we only care about tags but sometimes we care about heads too. 179 out, gitErr := Run(r.dir, "git", "ls-remote", "-q", r.remote) 180 if gitErr != nil { 181 if rerr, ok := gitErr.(*RunError); ok { 182 if bytes.Contains(rerr.Stderr, []byte("fatal: could not read Username")) { 183 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." 184 } 185 } 186 187 // If the remote URL doesn't exist at all, ideally we should treat the whole 188 // repository as nonexistent by wrapping the error in a notExistError. 189 // For HTTP and HTTPS, that's easy to detect: we'll try to fetch the URL 190 // ourselves and see what code it serves. 191 if u, err := url.Parse(r.remoteURL); err == nil && (u.Scheme == "http" || u.Scheme == "https") { 192 if _, err := web.GetBytes(u); errors.Is(err, fs.ErrNotExist) { 193 gitErr = notExistError{gitErr} 194 } 195 } 196 197 r.refsErr = gitErr 198 return 199 } 200 201 refs := make(map[string]string) 202 for _, line := range strings.Split(string(out), "\n") { 203 f := strings.Fields(line) 204 if len(f) != 2 { 205 continue 206 } 207 if f[1] == "HEAD" || strings.HasPrefix(f[1], "refs/heads/") || strings.HasPrefix(f[1], "refs/tags/") { 208 refs[f[1]] = f[0] 209 } 210 } 211 for ref, hash := range refs { 212 if strings.HasSuffix(ref, "^{}") { // record unwrapped annotated tag as value of tag 213 refs[strings.TrimSuffix(ref, "^{}")] = hash 214 delete(refs, ref) 215 } 216 } 217 r.refs = refs 218 }) 219 return r.refs, r.refsErr 220 } 221 222 func (r *gitRepo) Tags(prefix string) ([]string, error) { 223 refs, err := r.loadRefs() 224 if err != nil { 225 return nil, err 226 } 227 228 tags := []string{} 229 for ref := range refs { 230 if !strings.HasPrefix(ref, "refs/tags/") { 231 continue 232 } 233 tag := ref[len("refs/tags/"):] 234 if !strings.HasPrefix(tag, prefix) { 235 continue 236 } 237 tags = append(tags, tag) 238 } 239 sort.Strings(tags) 240 return tags, nil 241 } 242 243 func (r *gitRepo) Latest() (*RevInfo, error) { 244 refs, err := r.loadRefs() 245 if err != nil { 246 return nil, err 247 } 248 if refs["HEAD"] == "" { 249 return nil, ErrNoCommits 250 } 251 return r.Stat(refs["HEAD"]) 252 } 253 254 // findRef finds some ref name for the given hash, 255 // for use when the server requires giving a ref instead of a hash. 256 // There may be multiple ref names for a given hash, 257 // in which case this returns some name - it doesn't matter which. 258 func (r *gitRepo) findRef(hash string) (ref string, ok bool) { 259 refs, err := r.loadRefs() 260 if err != nil { 261 return "", false 262 } 263 for ref, h := range refs { 264 if h == hash { 265 return ref, true 266 } 267 } 268 return "", false 269 } 270 271 // minHashDigits is the minimum number of digits to require 272 // before accepting a hex digit sequence as potentially identifying 273 // a specific commit in a git repo. (Of course, users can always 274 // specify more digits, and many will paste in all 40 digits, 275 // but many of git's commands default to printing short hashes 276 // as 7 digits.) 277 const minHashDigits = 7 278 279 // stat stats the given rev in the local repository, 280 // or else it fetches more info from the remote repository and tries again. 281 func (r *gitRepo) stat(rev string) (*RevInfo, error) { 282 if r.local { 283 return r.statLocal(rev, rev) 284 } 285 286 // Fast path: maybe rev is a hash we already have locally. 287 didStatLocal := false 288 if len(rev) >= minHashDigits && len(rev) <= 40 && AllHex(rev) { 289 if info, err := r.statLocal(rev, rev); err == nil { 290 return info, nil 291 } 292 didStatLocal = true 293 } 294 295 // Maybe rev is a tag we already have locally. 296 // (Note that we're excluding branches, which can be stale.) 297 r.localTagsOnce.Do(r.loadLocalTags) 298 if r.localTags[rev] { 299 return r.statLocal(rev, "refs/tags/"+rev) 300 } 301 302 // Maybe rev is the name of a tag or branch on the remote server. 303 // Or maybe it's the prefix of a hash of a named ref. 304 // Try to resolve to both a ref (git name) and full (40-hex-digit) commit hash. 305 refs, err := r.loadRefs() 306 if err != nil { 307 return nil, err 308 } 309 // loadRefs may return an error if git fails, for example segfaults, or 310 // could not load a private repo, but defer checking to the else block 311 // below, in case we already have the rev in question in the local cache. 312 var ref, hash string 313 if refs["refs/tags/"+rev] != "" { 314 ref = "refs/tags/" + rev 315 hash = refs[ref] 316 // Keep rev as is: tags are assumed not to change meaning. 317 } else if refs["refs/heads/"+rev] != "" { 318 ref = "refs/heads/" + rev 319 hash = refs[ref] 320 rev = hash // Replace rev, because meaning of refs/heads/foo can change. 321 } else if rev == "HEAD" && refs["HEAD"] != "" { 322 ref = "HEAD" 323 hash = refs[ref] 324 rev = hash // Replace rev, because meaning of HEAD can change. 325 } else if len(rev) >= minHashDigits && len(rev) <= 40 && AllHex(rev) { 326 // At the least, we have a hash prefix we can look up after the fetch below. 327 // Maybe we can map it to a full hash using the known refs. 328 prefix := rev 329 // Check whether rev is prefix of known ref hash. 330 for k, h := range refs { 331 if strings.HasPrefix(h, prefix) { 332 if hash != "" && hash != h { 333 // Hash is an ambiguous hash prefix. 334 // More information will not change that. 335 return nil, fmt.Errorf("ambiguous revision %s", rev) 336 } 337 if ref == "" || ref > k { // Break ties deterministically when multiple refs point at same hash. 338 ref = k 339 } 340 rev = h 341 hash = h 342 } 343 } 344 if hash == "" && len(rev) == 40 { // Didn't find a ref, but rev is a full hash. 345 hash = rev 346 } 347 } else { 348 return nil, &UnknownRevisionError{Rev: rev} 349 } 350 351 // Protect r.fetchLevel and the "fetch more and more" sequence. 352 unlock, err := r.mu.Lock() 353 if err != nil { 354 return nil, err 355 } 356 defer unlock() 357 358 // Perhaps r.localTags did not have the ref when we loaded local tags, 359 // but we've since done fetches that pulled down the hash we need 360 // (or already have the hash we need, just without its tag). 361 // Either way, try a local stat before falling back to network I/O. 362 if !didStatLocal { 363 if info, err := r.statLocal(rev, hash); err == nil { 364 if strings.HasPrefix(ref, "refs/tags/") { 365 // Make sure tag exists, so it will be in localTags next time the go command is run. 366 Run(r.dir, "git", "tag", strings.TrimPrefix(ref, "refs/tags/"), hash) 367 } 368 return info, nil 369 } 370 } 371 372 // If we know a specific commit we need and its ref, fetch it. 373 // We do NOT fetch arbitrary hashes (when we don't know the ref) 374 // because we want to avoid ever importing a commit that isn't 375 // reachable from refs/tags/* or refs/heads/* or HEAD. 376 // Both Gerrit and GitHub expose every CL/PR as a named ref, 377 // and we don't want those commits masquerading as being real 378 // pseudo-versions in the main repo. 379 if r.fetchLevel <= fetchSome && ref != "" && hash != "" && !r.local { 380 r.fetchLevel = fetchSome 381 var refspec string 382 if ref != "" && ref != "HEAD" { 383 // If we do know the ref name, save the mapping locally 384 // so that (if it is a tag) it can show up in localTags 385 // on a future call. Also, some servers refuse to allow 386 // full hashes in ref specs, so prefer a ref name if known. 387 refspec = ref + ":" + ref 388 } else { 389 // Fetch the hash but give it a local name (refs/dummy), 390 // because that triggers the fetch behavior of creating any 391 // other known remote tags for the hash. We never use 392 // refs/dummy (it's not refs/tags/dummy) and it will be 393 // overwritten in the next command, and that's fine. 394 ref = hash 395 refspec = hash + ":refs/dummy" 396 } 397 _, err := Run(r.dir, "git", "fetch", "-f", "--depth=1", r.remote, refspec) 398 if err == nil { 399 return r.statLocal(rev, ref) 400 } 401 // Don't try to be smart about parsing the error. 402 // It's too complex and varies too much by git version. 403 // No matter what went wrong, fall back to a complete fetch. 404 } 405 406 // Last resort. 407 // Fetch all heads and tags and hope the hash we want is in the history. 408 if err := r.fetchRefsLocked(); err != nil { 409 return nil, err 410 } 411 412 return r.statLocal(rev, rev) 413 } 414 415 // fetchRefsLocked fetches all heads and tags from the origin, along with the 416 // ancestors of those commits. 417 // 418 // We only fetch heads and tags, not arbitrary other commits: we don't want to 419 // pull in off-branch commits (such as rejected GitHub pull requests) that the 420 // server may be willing to provide. (See the comments within the stat method 421 // for more detail.) 422 // 423 // fetchRefsLocked requires that r.mu remain locked for the duration of the call. 424 func (r *gitRepo) fetchRefsLocked() error { 425 if r.fetchLevel < fetchAll { 426 // NOTE: To work around a bug affecting Git clients up to at least 2.23.0 427 // (2019-08-16), we must first expand the set of local refs, and only then 428 // unshallow the repository as a separate fetch operation. (See 429 // golang.org/issue/34266 and 430 // https://github.com/git/git/blob/4c86140027f4a0d2caaa3ab4bd8bfc5ce3c11c8a/transport.c#L1303-L1309.) 431 432 if _, err := Run(r.dir, "git", "fetch", "-f", r.remote, "refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"); err != nil { 433 return err 434 } 435 436 if _, err := os.Stat(filepath.Join(r.dir, "shallow")); err == nil { 437 if _, err := Run(r.dir, "git", "fetch", "--unshallow", "-f", r.remote); err != nil { 438 return err 439 } 440 } 441 442 r.fetchLevel = fetchAll 443 } 444 return nil 445 } 446 447 // statLocal returns a RevInfo describing rev in the local git repository. 448 // It uses version as info.Version. 449 func (r *gitRepo) statLocal(version, rev string) (*RevInfo, error) { 450 out, err := Run(r.dir, "git", "-c", "log.showsignature=false", "log", "--no-decorate", "-n1", "--format=format:%H %ct %D", rev, "--") 451 if err != nil { 452 return nil, &UnknownRevisionError{Rev: rev} 453 } 454 f := strings.Fields(string(out)) 455 if len(f) < 2 { 456 return nil, fmt.Errorf("unexpected response from git log: %q", out) 457 } 458 hash := f[0] 459 if strings.HasPrefix(hash, version) { 460 version = hash // extend to full hash 461 } 462 t, err := strconv.ParseInt(f[1], 10, 64) 463 if err != nil { 464 return nil, fmt.Errorf("invalid time from git log: %q", out) 465 } 466 467 info := &RevInfo{ 468 Name: hash, 469 Short: ShortenSHA1(hash), 470 Time: time.Unix(t, 0).UTC(), 471 Version: hash, 472 } 473 474 // Add tags. Output looks like: 475 // ede458df7cd0fdca520df19a33158086a8a68e81 1523994202 HEAD -> master, tag: v1.2.4-annotated, tag: v1.2.3, origin/master, origin/HEAD 476 for i := 2; i < len(f); i++ { 477 if f[i] == "tag:" { 478 i++ 479 if i < len(f) { 480 info.Tags = append(info.Tags, strings.TrimSuffix(f[i], ",")) 481 } 482 } 483 } 484 sort.Strings(info.Tags) 485 486 // Used hash as info.Version above. 487 // Use caller's suggested version if it appears in the tag list 488 // (filters out branch names, HEAD). 489 for _, tag := range info.Tags { 490 if version == tag { 491 info.Version = version 492 } 493 } 494 495 return info, nil 496 } 497 498 func (r *gitRepo) Stat(rev string) (*RevInfo, error) { 499 if rev == "latest" { 500 return r.Latest() 501 } 502 type cached struct { 503 info *RevInfo 504 err error 505 } 506 c := r.statCache.Do(rev, func() interface{} { 507 info, err := r.stat(rev) 508 return cached{info, err} 509 }).(cached) 510 return c.info, c.err 511 } 512 513 func (r *gitRepo) ReadFile(rev, file string, maxSize int64) ([]byte, error) { 514 // TODO: Could use git cat-file --batch. 515 info, err := r.Stat(rev) // download rev into local git repo 516 if err != nil { 517 return nil, err 518 } 519 out, err := Run(r.dir, "git", "cat-file", "blob", info.Name+":"+file) 520 if err != nil { 521 return nil, fs.ErrNotExist 522 } 523 return out, nil 524 } 525 526 func (r *gitRepo) RecentTag(rev, prefix string, allowed func(string) bool) (tag string, err error) { 527 info, err := r.Stat(rev) 528 if err != nil { 529 return "", err 530 } 531 rev = info.Name // expand hash prefixes 532 533 // describe sets tag and err using 'git for-each-ref' and reports whether the 534 // result is definitive. 535 describe := func() (definitive bool) { 536 var out []byte 537 out, err = Run(r.dir, "git", "for-each-ref", "--format", "%(refname)", "refs/tags", "--merged", rev) 538 if err != nil { 539 return true 540 } 541 542 // prefixed tags aren't valid semver tags so compare without prefix, but only tags with correct prefix 543 var highest string 544 for _, line := range strings.Split(string(out), "\n") { 545 line = strings.TrimSpace(line) 546 // git do support lstrip in for-each-ref format, but it was added in v2.13.0. Stripping here 547 // instead gives support for git v2.7.0. 548 if !strings.HasPrefix(line, "refs/tags/") { 549 continue 550 } 551 line = line[len("refs/tags/"):] 552 553 if !strings.HasPrefix(line, prefix) { 554 continue 555 } 556 557 semtag := line[len(prefix):] 558 // Consider only tags that are valid and complete (not just major.minor prefixes). 559 // NOTE: Do not replace the call to semver.Compare with semver.Max. 560 // We want to return the actual tag, not a canonicalized version of it, 561 // and semver.Max currently canonicalizes (see golang.org/issue/32700). 562 if c := semver.Canonical(semtag); c == "" || !strings.HasPrefix(semtag, c) || !allowed(semtag) { 563 continue 564 } 565 if semver.Compare(semtag, highest) > 0 { 566 highest = semtag 567 } 568 } 569 570 if highest != "" { 571 tag = prefix + highest 572 } 573 574 return tag != "" && !AllHex(tag) 575 } 576 577 if describe() { 578 return tag, err 579 } 580 581 // Git didn't find a version tag preceding the requested rev. 582 // See whether any plausible tag exists. 583 tags, err := r.Tags(prefix + "v") 584 if err != nil { 585 return "", err 586 } 587 if len(tags) == 0 { 588 return "", nil 589 } 590 591 // There are plausible tags, but we don't know if rev is a descendent of any of them. 592 // Fetch the history to find out. 593 594 unlock, err := r.mu.Lock() 595 if err != nil { 596 return "", err 597 } 598 defer unlock() 599 600 if err := r.fetchRefsLocked(); err != nil { 601 return "", err 602 } 603 604 // If we've reached this point, we have all of the commits that are reachable 605 // from all heads and tags. 606 // 607 // The only refs we should be missing are those that are no longer reachable 608 // (or never were reachable) from any branch or tag, including the master 609 // branch, and we don't want to resolve them anyway (they're probably 610 // unreachable for a reason). 611 // 612 // Try one last time in case some other goroutine fetched rev while we were 613 // waiting on the lock. 614 describe() 615 return tag, err 616 } 617 618 func (r *gitRepo) DescendsFrom(rev, tag string) (bool, error) { 619 // The "--is-ancestor" flag was added to "git merge-base" in version 1.8.0, so 620 // this won't work with Git 1.7.1. According to golang.org/issue/28550, cmd/go 621 // already doesn't work with Git 1.7.1, so at least it's not a regression. 622 // 623 // git merge-base --is-ancestor exits with status 0 if rev is an ancestor, or 624 // 1 if not. 625 _, err := Run(r.dir, "git", "merge-base", "--is-ancestor", "--", tag, rev) 626 627 // Git reports "is an ancestor" with exit code 0 and "not an ancestor" with 628 // exit code 1. 629 // Unfortunately, if we've already fetched rev with a shallow history, git 630 // merge-base has been observed to report a false-negative, so don't stop yet 631 // even if the exit code is 1! 632 if err == nil { 633 return true, nil 634 } 635 636 // See whether the tag and rev even exist. 637 tags, err := r.Tags(tag) 638 if err != nil { 639 return false, err 640 } 641 if len(tags) == 0 { 642 return false, nil 643 } 644 645 // NOTE: r.stat is very careful not to fetch commits that we shouldn't know 646 // about, like rejected GitHub pull requests, so don't try to short-circuit 647 // that here. 648 if _, err = r.stat(rev); err != nil { 649 return false, err 650 } 651 652 // Now fetch history so that git can search for a path. 653 unlock, err := r.mu.Lock() 654 if err != nil { 655 return false, err 656 } 657 defer unlock() 658 659 if r.fetchLevel < fetchAll { 660 // Fetch the complete history for all refs and heads. It would be more 661 // efficient to only fetch the history from rev to tag, but that's much more 662 // complicated, and any kind of shallow fetch is fairly likely to trigger 663 // bugs in JGit servers and/or the go command anyway. 664 if err := r.fetchRefsLocked(); err != nil { 665 return false, err 666 } 667 } 668 669 _, err = Run(r.dir, "git", "merge-base", "--is-ancestor", "--", tag, rev) 670 if err == nil { 671 return true, nil 672 } 673 if ee, ok := err.(*RunError).Err.(*exec.ExitError); ok && ee.ExitCode() == 1 { 674 return false, nil 675 } 676 return false, err 677 } 678 679 func (r *gitRepo) ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, err error) { 680 // TODO: Use maxSize or drop it. 681 args := []string{} 682 if subdir != "" { 683 args = append(args, "--", subdir) 684 } 685 info, err := r.Stat(rev) // download rev into local git repo 686 if err != nil { 687 return nil, err 688 } 689 690 unlock, err := r.mu.Lock() 691 if err != nil { 692 return nil, err 693 } 694 defer unlock() 695 696 if err := ensureGitAttributes(r.dir); err != nil { 697 return nil, err 698 } 699 700 // Incredibly, git produces different archives depending on whether 701 // it is running on a Windows system or not, in an attempt to normalize 702 // text file line endings. Setting -c core.autocrlf=input means only 703 // translate files on the way into the repo, not on the way out (archive). 704 // The -c core.eol=lf should be unnecessary but set it anyway. 705 archive, err := Run(r.dir, "git", "-c", "core.autocrlf=input", "-c", "core.eol=lf", "archive", "--format=zip", "--prefix=prefix/", info.Name, args) 706 if err != nil { 707 if bytes.Contains(err.(*RunError).Stderr, []byte("did not match any files")) { 708 return nil, fs.ErrNotExist 709 } 710 return nil, err 711 } 712 713 return io.NopCloser(bytes.NewReader(archive)), nil 714 } 715 716 // ensureGitAttributes makes sure export-subst and export-ignore features are 717 // disabled for this repo. This is intended to be run prior to running git 718 // archive so that zip files are generated that produce consistent ziphashes 719 // for a given revision, independent of variables such as git version and the 720 // size of the repo. 721 // 722 // See: https://github.com/golang/go/issues/27153 723 func ensureGitAttributes(repoDir string) (err error) { 724 const attr = "\n* -export-subst -export-ignore\n" 725 726 d := repoDir + "/info" 727 p := d + "/attributes" 728 729 if err := os.MkdirAll(d, 0755); err != nil { 730 return err 731 } 732 733 f, err := os.OpenFile(p, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666) 734 if err != nil { 735 return err 736 } 737 defer func() { 738 closeErr := f.Close() 739 if closeErr != nil { 740 err = closeErr 741 } 742 }() 743 744 b, err := io.ReadAll(f) 745 if err != nil { 746 return err 747 } 748 if !bytes.HasSuffix(b, []byte(attr)) { 749 _, err := f.WriteString(attr) 750 return err 751 } 752 753 return nil 754 }