-
Notifications
You must be signed in to change notification settings - Fork 0
/
borrowed_git.go
153 lines (139 loc) · 4.64 KB
/
borrowed_git.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// Copyright (C) 2018 The Go Authors. All rights reserved.
// Copyright (C) 2021-2022 Ambassador Labs
// Copyright (C) 2023 Luke Shumaker <lukeshu@lukeshu.com>
//
// Use of this source code is governed by a BSD-style
// license that can be found in the NOTICE file.
//
// SPDX-License-Identifier: BSD-3-Clause
//
// This file is a lightly modified subset of Go 1.17
// cmd/go/internal/modfetch/codehost/git.go
package main
import (
"context"
"fmt"
"sort"
"strconv"
"strings"
"time"
"golang.org/x/mod/semver"
)
type commitInfo struct {
Hash string
Time time.Time
Tags []string
}
// statLocal is based on and closely mimics Go 1.17
// cmd/go/internal/modfetch/codehost/git.go:gitRepo.statLocal().
func statLocal(ctx context.Context, commitish string) (*commitInfo, error) {
if strings.HasPrefix(commitish, "-") {
return nil, &UnknownRevisionError{Rev: commitish}
}
out, err := cmdOutput(ctx, "git", "-c", "log.showsignature=false", "log", "-n1", "--format=format:%H %ct %D", commitish, "--")
if err != nil {
return nil, &UnknownRevisionError{Rev: commitish}
}
f := strings.Fields(out)
if len(f) < 2 {
return nil, fmt.Errorf("unexpected response from git log: %q", out)
}
hash := f[0]
t, err := strconv.ParseInt(f[1], 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid time from git log: %q", out)
}
info := commitInfo{
Hash: hash,
Time: time.Unix(t, 0).UTC(),
}
// Add tags. Output looks like:
// ede458df7cd0fdca520df19a33158086a8a68e81 1523994202 HEAD -> master, tag: v1.2.4-annotated, tag: v1.2.3, origin/master, origin/HEAD
for i := 2; i < len(f); i++ {
if f[i] == "tag:" {
i++
if i < len(f) {
info.Tags = append(info.Tags, strings.TrimSuffix(f[i], ","))
}
}
}
sort.Strings(info.Tags)
return &info, nil
}
// forEachReachableTag is based on and closely mimics Go 1.17
// cmd/go/internal/modfetch/codehost/git.go:gitRepo.RecentTag().
//
// forEachReachableTag iterates over all Git tags matching "refs/tags/{dirPrefix}v{SEMVER}" that are
// reachable-from (self-or-ancestor-of) the given `commit`, calling `cb` on each "v{SEMVER}" string
// (with any "refs/tags/" or {dirPrefix} prefix trimmed off). The iteration happens in no
// particular order.
func forEachReachableTag(ctx context.Context, commit *commitInfo, dirPrefix string, cb func(string)) error {
refPrefix := "refs/tags/" + dirPrefix
out, err := cmdOutput(ctx, "git", "for-each-ref",
"--format=%(refname)",
"--merged="+commit.Hash,
refPrefix)
if err != nil {
return err
}
for _, line := range strings.Split(out, "\n") {
line = strings.TrimSpace(line)
// Git does support lstrip in for-each-ref format, but it was added in v2.13.0.
// Stripping here instead gives support for git v2.7.0.
if !strings.HasPrefix(line, refPrefix) {
// This check mostly just handles the trailing blank line and could be
// `if line != ""`, but let's have the full HasPrefix check just to be safe.
continue
}
semtag := line[len(refPrefix):]
// Consider only tags that are valid and complete (not just major.minor prefixes).
if c := semver.Canonical(semtag); c == "" || !strings.HasPrefix(semtag, c) {
continue
}
cb(semtag)
}
return nil
}
// mostRecentTag is based on and closely mimics Go 1.17
// cmd/go/internal/modfetch/codehost/git.go:gitRepo.RecentTag().
//
// The word "recent" is a little bit of a lie; it's based on semver ordering, not commit timestamps.
func mostRecentTag(ctx context.Context, commit *commitInfo, dirPrefix string) (string, error) {
var highest string
err := forEachReachableTag(ctx, commit, dirPrefix, func(semtag string) {
// NOTE: Do not replace the call to semver.Compare with semver.Max.
// We want to return the actual tag, not a canonicalized version of it,
// and semver.Max currently canonicalizes (see golang.org/issue/32700).
if semver.Compare(semtag, highest) > 0 {
highest = semtag
}
})
if err != nil {
return "", err
}
if highest == "" {
return "", nil
}
return dirPrefix + highest, nil
}
// mostRecentTags is like mostRecentTag, but returns a list of tags ordered most-recent-first,
// rather than returning the singular most-recent tag.
//
// Like with mostRecentTag, the word "recent" is a little bit of a lie; it's based on semver
// ordering, not commit timestamps.
func mostRecentTags(ctx context.Context, commit *commitInfo, dirPrefix string) ([]string, error) {
var semtags []string
err := forEachReachableTag(ctx, commit, dirPrefix, func(semtag string) {
semtags = append(semtags, semtag)
})
if err != nil {
return nil, err
}
if len(semtags) == 0 {
return nil, nil
}
sort.SliceStable(semtags, func(i, j int) bool {
return semver.Compare(semtags[i], semtags[j]) > 0
})
return semtags, nil
}