1
2
3
4
5 package codehost
6
7 import (
8 "bytes"
9 "context"
10 "crypto/sha256"
11 "encoding/base64"
12 "errors"
13 "fmt"
14 "io"
15 "io/fs"
16 "net/url"
17 "os"
18 "os/exec"
19 "path/filepath"
20 "regexp"
21 "runtime"
22 "slices"
23 "sort"
24 "strconv"
25 "strings"
26 "sync"
27 "time"
28
29 "cmd/go/internal/base"
30 "cmd/go/internal/lockedfile"
31 "cmd/go/internal/web"
32 "cmd/internal/par"
33
34 "golang.org/x/mod/semver"
35 )
36
37
38
39 type notExistError struct {
40 err error
41 }
42
43 func (e notExistError) Error() string { return e.err.Error() }
44 func (notExistError) Is(err error) bool { return err == fs.ErrNotExist }
45
46 const gitWorkDirType = "git3"
47
48 func newGitRepo(ctx context.Context, remote string, local bool) (Repo, error) {
49 r := &gitRepo{remote: remote, local: local}
50 if local {
51 if strings.Contains(remote, "://") {
52 return nil, fmt.Errorf("git remote (%s) lookup disabled", remote)
53 }
54 info, err := os.Stat(remote)
55 if err != nil {
56 return nil, err
57 }
58 if !info.IsDir() {
59 return nil, fmt.Errorf("%s exists but is not a directory", remote)
60 }
61 r.dir = remote
62 r.mu.Path = r.dir + ".lock"
63 r.sha256Hashes = r.checkConfigSHA256(ctx)
64 return r, nil
65 }
66
67 if !strings.Contains(remote, "://") {
68 if strings.Contains(remote, ":") {
69 return nil, fmt.Errorf("git remote (%s) must not be local directory (use URL syntax not host:path syntax)", remote)
70 }
71 return nil, fmt.Errorf("git remote (%s) must not be local directory", remote)
72 }
73 var err error
74 r.dir, r.mu.Path, err = WorkDir(ctx, 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 repoSha256Hash := false
87 if refs, lrErr := r.loadRefs(ctx); lrErr == nil {
88
89
90 for _, refHash := range refs {
91 repoSha256Hash = len(refHash) == (256 / 4)
92 break
93 }
94 }
95 gitSupportsSHA256, gitVersErr := gitSupportsSHA256()
96 if gitVersErr != nil {
97 return nil, fmt.Errorf("unable to resolve git version: %w", gitVersErr)
98 }
99 objFormatFlag := []string{}
100
101
102 if repoSha256Hash {
103
104
105
106
107 objFormatFlag = []string{"--object-format=sha256"}
108 } else if gitSupportsSHA256 {
109 objFormatFlag = []string{"--object-format=sha1"}
110 }
111 if _, err := Run(ctx, r.dir, "git", "init", "--bare", objFormatFlag); err != nil {
112 os.RemoveAll(r.dir)
113 return nil, err
114 }
115
116
117
118
119 if _, err := r.runGit(ctx, "git", "remote", "add", "origin", "--", r.remote); err != nil {
120 os.RemoveAll(r.dir)
121 return nil, err
122 }
123 if runtime.GOOS == "windows" {
124
125
126
127
128
129
130
131
132
133 if _, err := r.runGit(ctx, "git", "config", "core.longpaths", "true"); err != nil {
134 os.RemoveAll(r.dir)
135 return nil, err
136 }
137 }
138 }
139 r.sha256Hashes = r.checkConfigSHA256(ctx)
140 r.remoteURL = r.remote
141 r.remote = "origin"
142 return r, nil
143 }
144
145 type gitRepo struct {
146 ctx context.Context
147
148 remote, remoteURL string
149 local bool
150 dir string
151
152
153 sha256Hashes bool
154
155 mu lockedfile.Mutex
156
157 fetchLevel int
158
159 statCache par.ErrCache[string, *RevInfo]
160
161 refsOnce sync.Once
162
163
164 refs map[string]string
165 refsErr error
166
167 localTagsOnce sync.Once
168 localTags sync.Map
169 }
170
171 const (
172
173 fetchNone = iota
174 fetchSome
175 fetchAll
176 )
177
178
179
180 func (r *gitRepo) loadLocalTags(ctx context.Context) {
181
182
183
184 out, err := r.runGit(ctx, "git", "tag", "-l")
185 if err != nil {
186 return
187 }
188
189 for line := range strings.SplitSeq(string(out), "\n") {
190 if line != "" {
191 r.localTags.Store(line, true)
192 }
193 }
194 }
195
196 func (r *gitRepo) CheckReuse(ctx context.Context, old *Origin, subdir string) error {
197 if old == nil {
198 return fmt.Errorf("missing origin")
199 }
200 if old.VCS != "git" || old.URL != r.remoteURL {
201 return fmt.Errorf("origin moved from %v %q to %v %q", old.VCS, old.URL, "git", r.remoteURL)
202 }
203 if old.Subdir != subdir {
204 return fmt.Errorf("origin moved from %v %q %q to %v %q %q", old.VCS, old.URL, old.Subdir, "git", r.remoteURL, subdir)
205 }
206
207
208
209
210
211
212 if old.Hash == "" && old.TagSum == "" && old.RepoSum == "" {
213 return fmt.Errorf("non-specific origin")
214 }
215
216 r.loadRefs(ctx)
217 if r.refsErr != nil {
218 return r.refsErr
219 }
220
221 if old.Ref != "" {
222 hash, ok := r.refs[old.Ref]
223 if !ok {
224 return fmt.Errorf("ref %q deleted", old.Ref)
225 }
226 if hash != old.Hash {
227 return fmt.Errorf("ref %q moved from %s to %s", old.Ref, old.Hash, hash)
228 }
229 }
230 if old.TagSum != "" {
231 tags, err := r.Tags(ctx, old.TagPrefix)
232 if err != nil {
233 return err
234 }
235 if tags.Origin.TagSum != old.TagSum {
236 return fmt.Errorf("tags changed")
237 }
238 }
239 if old.RepoSum != "" {
240 if r.repoSum(r.refs) != old.RepoSum {
241 return fmt.Errorf("refs changed")
242 }
243 }
244 return nil
245 }
246
247
248
249 func (r *gitRepo) loadRefs(ctx context.Context) (map[string]string, error) {
250 if r.local {
251
252
253 return nil, nil
254 }
255 r.refsOnce.Do(func() {
256
257
258
259 release, err := base.AcquireNet()
260 if err != nil {
261 r.refsErr = err
262 return
263 }
264 out, gitErr := r.runGit(ctx, "git", "ls-remote", "-q", r.remote)
265 release()
266
267 if gitErr != nil {
268 if rerr, ok := gitErr.(*RunError); ok {
269 if bytes.Contains(rerr.Stderr, []byte("fatal: could not read Username")) {
270 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."
271 }
272 }
273
274
275
276
277
278 if u, err := url.Parse(r.remoteURL); err == nil && (u.Scheme == "http" || u.Scheme == "https") {
279 if _, err := web.GetBytes(u); errors.Is(err, fs.ErrNotExist) {
280 gitErr = notExistError{gitErr}
281 }
282 }
283
284 r.refsErr = gitErr
285 return
286 }
287
288 refs := make(map[string]string)
289 for line := range strings.SplitSeq(string(out), "\n") {
290 f := strings.Fields(line)
291 if len(f) != 2 {
292 continue
293 }
294 if f[1] == "HEAD" || strings.HasPrefix(f[1], "refs/heads/") || strings.HasPrefix(f[1], "refs/tags/") {
295 refs[f[1]] = f[0]
296 }
297 }
298 for ref, hash := range refs {
299 if k, found := strings.CutSuffix(ref, "^{}"); found {
300 refs[k] = hash
301 delete(refs, ref)
302 }
303 }
304 r.refs = refs
305 })
306 return r.refs, r.refsErr
307 }
308
309 func (r *gitRepo) Tags(ctx context.Context, prefix string) (*Tags, error) {
310 refs, err := r.loadRefs(ctx)
311 if err != nil {
312 return nil, err
313 }
314
315 tags := &Tags{
316 Origin: &Origin{
317 VCS: "git",
318 URL: r.remoteURL,
319 TagPrefix: prefix,
320 },
321 List: []Tag{},
322 }
323 for ref, hash := range refs {
324 if !strings.HasPrefix(ref, "refs/tags/") {
325 continue
326 }
327 tag := ref[len("refs/tags/"):]
328 if !strings.HasPrefix(tag, prefix) {
329 continue
330 }
331 tags.List = append(tags.List, Tag{tag, hash})
332 }
333 sort.Slice(tags.List, func(i, j int) bool {
334 return tags.List[i].Name < tags.List[j].Name
335 })
336
337 dir := prefix[:strings.LastIndex(prefix, "/")+1]
338 h := sha256.New()
339 for _, tag := range tags.List {
340 if isOriginTag(strings.TrimPrefix(tag.Name, dir)) {
341 fmt.Fprintf(h, "%q %s\n", tag.Name, tag.Hash)
342 }
343 }
344 tags.Origin.TagSum = "t1:" + base64.StdEncoding.EncodeToString(h.Sum(nil))
345 return tags, nil
346 }
347
348
349
350
351
352 func (r *gitRepo) repoSum(refs map[string]string) string {
353 list := make([]string, 0, len(refs))
354 for ref := range refs {
355 list = append(list, ref)
356 }
357 sort.Strings(list)
358 h := sha256.New()
359 for _, ref := range list {
360 fmt.Fprintf(h, "%q %s\n", ref, refs[ref])
361 }
362 return "r1:" + base64.StdEncoding.EncodeToString(h.Sum(nil))
363 }
364
365
366
367 func (r *gitRepo) unknownRevisionInfo(refs map[string]string) *RevInfo {
368 return &RevInfo{
369 Origin: &Origin{
370 VCS: "git",
371 URL: r.remoteURL,
372 RepoSum: r.repoSum(refs),
373 },
374 }
375 }
376
377 func (r *gitRepo) Latest(ctx context.Context) (*RevInfo, error) {
378 refs, err := r.loadRefs(ctx)
379 if err != nil {
380 return nil, err
381 }
382 if refs["HEAD"] == "" {
383 return nil, ErrNoCommits
384 }
385 statInfo, err := r.Stat(ctx, refs["HEAD"])
386 if err != nil {
387 return nil, err
388 }
389
390
391 info := new(RevInfo)
392 *info = *statInfo
393 info.Origin = new(Origin)
394 if statInfo.Origin != nil {
395 *info.Origin = *statInfo.Origin
396 }
397 info.Origin.Ref = "HEAD"
398 info.Origin.Hash = refs["HEAD"]
399
400 return info, nil
401 }
402
403 func (r *gitRepo) checkConfigSHA256(ctx context.Context) bool {
404 if hashType, sha256CfgErr := r.runGit(ctx, "git", "config", "extensions.objectformat"); sha256CfgErr == nil {
405 return strings.TrimSpace(string(hashType)) == "sha256"
406 }
407 return false
408 }
409
410 func (r *gitRepo) hexHashLen() int {
411 if !r.sha256Hashes {
412 return 160 / 4
413 }
414 return 256 / 4
415 }
416
417
418
419 func (r *gitRepo) shortenObjectHash(rev string) string {
420 if !r.sha256Hashes {
421 return ShortenSHA1(rev)
422 }
423 if AllHex(rev) && len(rev) == 256/4 {
424 return rev[:12]
425 }
426 return rev
427 }
428
429
430
431
432
433
434
435 const minHashDigits = 7
436
437
438
439 func (r *gitRepo) stat(ctx context.Context, rev string) (info *RevInfo, err error) {
440
441 didStatLocal := false
442 if len(rev) >= minHashDigits && len(rev) <= r.hexHashLen() && AllHex(rev) {
443 if info, err := r.statLocal(ctx, rev, rev); err == nil {
444 return info, nil
445 }
446 didStatLocal = true
447 }
448
449
450
451 r.localTagsOnce.Do(func() { r.loadLocalTags(ctx) })
452 if _, ok := r.localTags.Load(rev); ok {
453 return r.statLocal(ctx, rev, "refs/tags/"+rev)
454 }
455
456
457
458
459
460 refs, err := r.loadRefs(ctx)
461 if err != nil {
462 return nil, err
463 }
464
465
466
467 var ref, hash string
468 if refs["refs/tags/"+rev] != "" {
469 ref = "refs/tags/" + rev
470 hash = refs[ref]
471
472 } else if refs["refs/heads/"+rev] != "" {
473 ref = "refs/heads/" + rev
474 hash = refs[ref]
475 rev = hash
476 } else if rev == "HEAD" && refs["HEAD"] != "" {
477 ref = "HEAD"
478 hash = refs[ref]
479 rev = hash
480 } else if len(rev) >= minHashDigits && len(rev) <= r.hexHashLen() && AllHex(rev) {
481
482
483 prefix := rev
484
485 for k, h := range refs {
486 if strings.HasPrefix(h, prefix) {
487 if hash != "" && hash != h {
488
489
490 return nil, fmt.Errorf("ambiguous revision %s", rev)
491 }
492 if ref == "" || ref > k {
493 ref = k
494 }
495 rev = h
496 hash = h
497 }
498 }
499 if hash == "" && len(rev) == r.hexHashLen() {
500 hash = rev
501 }
502 } else {
503 return r.unknownRevisionInfo(refs), &UnknownRevisionError{Rev: rev}
504 }
505
506 defer func() {
507 if info != nil {
508 info.Origin.Hash = info.Name
509
510 if ref != info.Origin.Hash {
511 info.Origin.Ref = ref
512 }
513 }
514 }()
515
516
517 unlock, err := r.mu.Lock()
518 if err != nil {
519 return nil, err
520 }
521 defer unlock()
522
523
524
525
526
527 if !didStatLocal {
528 if info, err := r.statLocal(ctx, rev, hash); err == nil {
529 tag, fromTag := strings.CutPrefix(ref, "refs/tags/")
530 if fromTag && !slices.Contains(info.Tags, tag) {
531
532
533 _, err := r.runGit(ctx, "git", "tag", tag, hash)
534 if err != nil {
535 return nil, err
536 }
537 r.localTags.Store(tag, true)
538 return r.statLocal(ctx, rev, ref)
539 }
540 return info, err
541 }
542 }
543
544 if r.local {
545 return nil, fmt.Errorf("revision does not exist locally: %s", rev)
546 }
547
548
549
550
551
552
553
554
555 if r.fetchLevel <= fetchSome && ref != "" && hash != "" {
556 r.fetchLevel = fetchSome
557 var refspec string
558 if ref == "HEAD" {
559
560
561
562
563
564 ref = hash
565 refspec = hash + ":refs/dummy"
566 } else {
567
568
569
570
571 refspec = ref + ":" + ref
572 }
573
574 release, err := base.AcquireNet()
575 if err != nil {
576 return nil, err
577 }
578
579
580
581
582 _, err = r.runGit(ctx, "git", "-c", "protocol.version=2", "fetch", "-f", "--depth=1", r.remote, refspec)
583 release()
584
585 if err == nil {
586 return r.statLocal(ctx, rev, ref)
587 }
588
589
590
591 }
592
593
594
595 if err := r.fetchRefsLocked(ctx); err != nil {
596 return nil, err
597 }
598
599 return r.statLocal(ctx, rev, rev)
600 }
601
602
603
604
605
606
607
608
609
610
611 func (r *gitRepo) fetchRefsLocked(ctx context.Context) error {
612 if r.local {
613 panic("go: fetchRefsLocked called in local only mode.")
614 }
615 if r.fetchLevel < fetchAll {
616
617
618
619
620
621
622 release, err := base.AcquireNet()
623 if err != nil {
624 return err
625 }
626 defer release()
627
628 if _, err := r.runGit(ctx, "git", "fetch", "-f", r.remote, "refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"); err != nil {
629 return err
630 }
631
632 if _, err := os.Stat(filepath.Join(r.dir, "shallow")); err == nil {
633 if _, err := r.runGit(ctx, "git", "fetch", "--unshallow", "-f", r.remote); err != nil {
634 return err
635 }
636 }
637
638 r.fetchLevel = fetchAll
639 }
640 return nil
641 }
642
643
644
645 func (r *gitRepo) statLocal(ctx context.Context, version, rev string) (*RevInfo, error) {
646 out, err := r.runGit(ctx, "git", "-c", "log.showsignature=false", "log", "--no-decorate", "-n1", "--format=format:%H %ct %D", rev, "--")
647 if err != nil {
648
649 var info *RevInfo
650 if refs, err := r.loadRefs(ctx); err == nil {
651 info = r.unknownRevisionInfo(refs)
652 }
653 return info, &UnknownRevisionError{Rev: rev}
654 }
655 f := strings.Fields(string(out))
656 if len(f) < 2 {
657 return nil, fmt.Errorf("unexpected response from git log: %q", out)
658 }
659 hash := f[0]
660 if strings.HasPrefix(hash, version) {
661 version = hash
662 }
663 t, err := strconv.ParseInt(f[1], 10, 64)
664 if err != nil {
665 return nil, fmt.Errorf("invalid time from git log: %q", out)
666 }
667
668 info := &RevInfo{
669 Origin: &Origin{
670 VCS: "git",
671 URL: r.remoteURL,
672 Hash: hash,
673 },
674 Name: hash,
675 Short: r.shortenObjectHash(hash),
676 Time: time.Unix(t, 0).UTC(),
677 Version: hash,
678 }
679 if !strings.HasPrefix(hash, rev) {
680 info.Origin.Ref = rev
681 }
682
683
684
685 for i := 2; i < len(f); i++ {
686 if f[i] == "tag:" {
687 i++
688 if i < len(f) {
689 info.Tags = append(info.Tags, strings.TrimSuffix(f[i], ","))
690 }
691 }
692 }
693
694
695
696
697 if refs, err := r.loadRefs(ctx); err == nil {
698 for ref, h := range refs {
699 if h == hash {
700 if tag, found := strings.CutPrefix(ref, "refs/tags/"); found {
701 info.Tags = append(info.Tags, tag)
702 }
703 }
704 }
705 }
706 slices.Sort(info.Tags)
707 info.Tags = slices.Compact(info.Tags)
708
709
710
711
712 for _, tag := range info.Tags {
713 if version == tag {
714 info.Version = version
715 }
716 }
717
718 return info, nil
719 }
720
721 func (r *gitRepo) Stat(ctx context.Context, rev string) (*RevInfo, error) {
722 if rev == "latest" {
723 return r.Latest(ctx)
724 }
725 return r.statCache.Do(rev, func() (*RevInfo, error) {
726 return r.stat(ctx, rev)
727 })
728 }
729
730 func (r *gitRepo) ReadFile(ctx context.Context, rev, file string, maxSize int64) ([]byte, error) {
731
732 info, err := r.Stat(ctx, rev)
733 if err != nil {
734 return nil, err
735 }
736 out, err := r.runGit(ctx, "git", "cat-file", "blob", info.Name+":"+file)
737 if err != nil {
738 return nil, fs.ErrNotExist
739 }
740 return out, nil
741 }
742
743 func (r *gitRepo) RecentTag(ctx context.Context, rev, prefix string, allowed func(tag string) bool) (tag string, err error) {
744 info, err := r.Stat(ctx, rev)
745 if err != nil {
746 return "", err
747 }
748 rev = info.Name
749
750
751
752 describe := func() (definitive bool) {
753 var out []byte
754 out, err = r.runGit(ctx, "git", "for-each-ref", "--format", "%(refname)", "refs/tags", "--merged", rev)
755 if err != nil {
756 return true
757 }
758
759
760 var highest string
761 for line := range strings.SplitSeq(string(out), "\n") {
762 line = strings.TrimSpace(line)
763
764
765 if !strings.HasPrefix(line, "refs/tags/") {
766 continue
767 }
768 line = line[len("refs/tags/"):]
769
770 if !strings.HasPrefix(line, prefix) {
771 continue
772 }
773 if !allowed(line) {
774 continue
775 }
776
777 semtag := line[len(prefix):]
778 if semver.Compare(semtag, highest) > 0 {
779 highest = semtag
780 }
781 }
782
783 if highest != "" {
784 tag = prefix + highest
785 }
786
787 return tag != "" && !AllHex(tag)
788 }
789
790 if describe() {
791 return tag, err
792 }
793
794
795
796 tags, err := r.Tags(ctx, prefix+"v")
797 if err != nil {
798 return "", err
799 }
800 if len(tags.List) == 0 {
801 return "", nil
802 }
803
804 if r.local {
805 return "", fmt.Errorf("revision does not exist locally: %s", rev)
806 }
807
808
809
810
811
812
813 unlock, err := r.mu.Lock()
814 if err != nil {
815 return "", err
816 }
817 if err := r.fetchRefsLocked(ctx); err != nil {
818 unlock()
819 return "", err
820 }
821 unlock()
822
823
824
825
826
827
828
829
830
831
832
833 describe()
834 return tag, err
835 }
836
837 func (r *gitRepo) DescendsFrom(ctx context.Context, rev, tag string) (bool, error) {
838
839
840
841
842
843
844 _, err := r.runGit(ctx, "git", "merge-base", "--is-ancestor", "--", tag, rev)
845
846
847
848
849
850
851 if err == nil {
852 return true, nil
853 }
854
855
856 tags, err := r.Tags(ctx, tag)
857 if err != nil {
858 return false, err
859 }
860 if len(tags.List) == 0 {
861 return false, nil
862 }
863
864
865
866
867 if _, err = r.stat(ctx, rev); err != nil {
868 return false, err
869 }
870
871 if r.local {
872 return false, fmt.Errorf("revision does not exist locally: %s", rev)
873 }
874
875
876 unlock, err := r.mu.Lock()
877 if err != nil {
878 return false, err
879 }
880 defer unlock()
881
882 if r.fetchLevel < fetchAll {
883
884
885
886
887 if err := r.fetchRefsLocked(ctx); err != nil {
888 return false, err
889 }
890 }
891
892 _, err = r.runGit(ctx, "git", "merge-base", "--is-ancestor", "--", tag, rev)
893 if err == nil {
894 return true, nil
895 }
896 if ee, ok := err.(*RunError).Err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
897 return false, nil
898 }
899 return false, err
900 }
901
902 func (r *gitRepo) ReadZip(ctx context.Context, rev, subdir string, maxSize int64) (zip io.ReadCloser, err error) {
903
904 args := []string{}
905 if subdir != "" {
906 args = append(args, "--", subdir)
907 }
908 info, err := r.Stat(ctx, rev)
909 if err != nil {
910 return nil, err
911 }
912
913 unlock, err := r.mu.Lock()
914 if err != nil {
915 return nil, err
916 }
917 defer unlock()
918
919 if err := ensureGitAttributes(r.dir); err != nil {
920 return nil, err
921 }
922
923
924
925
926
927
928 archive, err := r.runGit(ctx, "git", "-c", "core.autocrlf=input", "-c", "core.eol=lf", "archive", "--format=zip", "--prefix=prefix/", info.Name, args)
929 if err != nil {
930 if bytes.Contains(err.(*RunError).Stderr, []byte("did not match any files")) {
931 return nil, fs.ErrNotExist
932 }
933 return nil, err
934 }
935
936 return io.NopCloser(bytes.NewReader(archive)), nil
937 }
938
939
940
941
942
943
944
945
946 func ensureGitAttributes(repoDir string) (err error) {
947 const attr = "\n* -export-subst -export-ignore\n"
948
949 d := repoDir + "/info"
950 p := d + "/attributes"
951
952 if err := os.MkdirAll(d, 0755); err != nil {
953 return err
954 }
955
956 f, err := os.OpenFile(p, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
957 if err != nil {
958 return err
959 }
960 defer func() {
961 closeErr := f.Close()
962 if closeErr != nil {
963 err = closeErr
964 }
965 }()
966
967 b, err := io.ReadAll(f)
968 if err != nil {
969 return err
970 }
971 if !bytes.HasSuffix(b, []byte(attr)) {
972 _, err := f.WriteString(attr)
973 return err
974 }
975
976 return nil
977 }
978
979 func (r *gitRepo) runGit(ctx context.Context, cmdline ...any) ([]byte, error) {
980 args := RunArgs{cmdline: cmdline, dir: r.dir, local: r.local}
981 if !r.local {
982
983
984 args.env = []string{"GIT_DIR=" + r.dir}
985 }
986 return RunWithArgs(ctx, args)
987 }
988
989
990 var gitVersLineExtract = regexp.MustCompile(`git version\s+(\d+\.\d+(?:\.\d+)?)`)
991
992 func gitVersion() (string, error) {
993 gitOut, runErr := exec.Command("git", "version").CombinedOutput()
994 if runErr != nil {
995 return "v0", fmt.Errorf("failed to execute git version: %w", runErr)
996 }
997 return extractGitVersion(gitOut)
998 }
999
1000 func extractGitVersion(gitOut []byte) (string, error) {
1001 matches := gitVersLineExtract.FindSubmatch(gitOut)
1002 if len(matches) < 2 {
1003 return "v0", fmt.Errorf("git version extraction regexp did not match version line: %q", gitOut)
1004 }
1005 return "v" + string(matches[1]), nil
1006 }
1007
1008 func hasAtLeastGitVersion(minVers string) (bool, error) {
1009 gitVers, gitVersErr := gitVersion()
1010 if gitVersErr != nil {
1011 return false, gitVersErr
1012 }
1013 return semver.Compare(minVers, gitVers) <= 0, nil
1014 }
1015
1016 const minGitSHA256Vers = "v2.29"
1017
1018 func gitSupportsSHA256() (bool, error) {
1019 return hasAtLeastGitVersion(minGitSHA256Vers)
1020 }
1021
View as plain text