-
Notifications
You must be signed in to change notification settings - Fork 132
/
git.go
300 lines (281 loc) · 8.52 KB
/
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
package warehouses
import (
"context"
"regexp"
"sort"
"strings"
"github.com/Masterminds/semver"
"github.com/pkg/errors"
kargoapi "github.com/akuity/kargo/api/v1alpha1"
"github.com/akuity/kargo/internal/controller/git"
"github.com/akuity/kargo/internal/credentials"
"github.com/akuity/kargo/internal/logging"
)
type gitMeta struct {
Commit string
Tag string
Message string
Author string
}
func (r *reconciler) selectCommits(
ctx context.Context,
namespace string,
subs []kargoapi.RepoSubscription,
) ([]kargoapi.GitCommit, error) {
latestCommits := make([]kargoapi.GitCommit, 0, len(subs))
for _, s := range subs {
if s.Git == nil {
continue
}
sub := s.Git
logger := logging.LoggerFromContext(ctx).WithField("repo", sub.RepoURL)
creds, ok, err :=
r.credentialsDB.Get(ctx, namespace, credentials.TypeGit, sub.RepoURL)
if err != nil {
return nil, errors.Wrapf(
err,
"error obtaining credentials for git repo %q",
sub.RepoURL,
)
}
var repoCreds *git.RepoCredentials
if ok {
repoCreds = &git.RepoCredentials{
Username: creds.Username,
Password: creds.Password,
SSHPrivateKey: creds.SSHPrivateKey,
}
logger.Debug("obtained credentials for git repo")
} else {
logger.Debug("found no credentials for git repo")
}
gm, err := r.selectCommitMetaFn(ctx, *s.Git, repoCreds)
if err != nil {
return nil, errors.Wrapf(
err,
"error determining latest commit ID of git repo %q",
sub.RepoURL,
)
}
logger.WithField("commit", gm.Commit).
Debug("found latest commit from repo")
latestCommits = append(
latestCommits,
kargoapi.GitCommit{
RepoURL: sub.RepoURL,
ID: gm.Commit,
Branch: sub.Branch,
Tag: gm.Tag,
Message: gm.Message,
},
)
}
return latestCommits, nil
}
// selectCommitMeta uses criteria from the provided GitSubscription to select
// an appropriate revision of the repository also specified by the subscription
// and return metadata associated with that revision.
func (r *reconciler) selectCommitMeta(
ctx context.Context,
sub kargoapi.GitSubscription,
creds *git.RepoCredentials,
) (*gitMeta, error) {
logger := logging.LoggerFromContext(ctx).WithField("repo", sub.RepoURL)
if creds == nil {
creds = &git.RepoCredentials{}
}
if sub.CommitSelectionStrategy == "" {
sub.CommitSelectionStrategy = kargoapi.CommitSelectionStrategyNewestFromBranch
}
repo, err := git.Clone(
sub.RepoURL,
*creds,
&git.CloneOptions{
Branch: sub.Branch,
SingleBranch: true,
Shallow: true,
InsecureSkipTLSVerify: sub.InsecureSkipTLSVerify,
},
)
if err != nil {
return nil, errors.Wrapf(err, "error cloning git repo %q", sub.RepoURL)
}
selectedTag, selectedCommit, err := r.selectTagAndCommitID(repo, sub)
if err != nil {
return nil, errors.Wrapf(
err,
"error selecting commit from git repo %q",
sub.RepoURL,
)
}
msg, err := repo.CommitMessage(selectedCommit)
if err != nil {
// This is best effort, so just log the error
logger.Warnf("failed to get message from commit %q: %v", selectedCommit, err)
}
return &gitMeta{
Commit: selectedCommit,
Tag: selectedTag,
// Since we currently store commit messages in Stage status, we only capture
// the first line of the commit message for brevity
Message: strings.Split(strings.TrimSpace(msg), "\n")[0],
// TODO: support git author
}, nil
}
// selectTagAndCommitID uses criteria from the provided GitSubscription to
// select and return an appropriate revision of the repository also specified by
// the subscription.
func (r *reconciler) selectTagAndCommitID(
repo git.Repo,
sub kargoapi.GitSubscription,
) (string, string, error) {
if sub.CommitSelectionStrategy == kargoapi.CommitSelectionStrategyNewestFromBranch {
// In this case, there is nothing to do except return the commit ID at the
// head of the branch.
commit, err := r.getLastCommitIDFn(repo)
return "", commit, errors.Wrapf(
err,
"error determining commit ID at head of branch %q in git repo %q",
sub.Branch,
sub.RepoURL,
)
}
tags, err := r.listTagsFn(repo) // These are ordered newest to oldest
if err != nil {
return "", "",
errors.Wrapf(err, "error listing tags from git repo %q", sub.RepoURL)
}
// Narrow down the list of tags to those that are allowed and not ignored
allowRegex, err := regexp.Compile(sub.AllowTags)
if err != nil {
return "", "",
errors.Wrapf(err, "error compiling regular expression %q", sub.AllowTags)
}
filteredTags := make([]string, 0, len(tags))
for _, tagName := range tags {
if allows(tagName, allowRegex) && !ignores(tagName, sub.IgnoreTags) {
filteredTags = append(filteredTags, tagName)
}
}
if len(filteredTags) == 0 {
return "", "",
errors.Errorf("found no applicable tags in repo %q", sub.RepoURL)
}
var selectedTag string
switch sub.CommitSelectionStrategy {
case kargoapi.CommitSelectionStrategyLexical:
selectedTag = selectLexicallyLastTag(filteredTags)
case kargoapi.CommitSelectionStrategyNewestTag:
selectedTag = filteredTags[0] // These are already ordered newest to oldest
case kargoapi.CommitSelectionStrategySemVer:
if selectedTag, err =
selectSemverTag(filteredTags, sub.SemverConstraint); err != nil {
return "", "", err
}
default:
return "", "", errors.Errorf(
"unknown commit selection strategy %q",
sub.CommitSelectionStrategy,
)
}
if selectedTag == "" {
return "", "", errors.Errorf("found no applicable tags in repo %q", sub.RepoURL)
}
// Checkout the selected tag and return the commit ID
if err = r.checkoutTagFn(repo, selectedTag); err != nil {
return "", "", errors.Wrapf(
err,
"error checking out tag %q from git repo %q",
selectedTag,
sub.RepoURL,
)
}
commit, err := r.getLastCommitIDFn(repo)
return selectedTag, commit, errors.Wrapf(
err,
"error determining commit ID of tag %q in git repo %q",
selectedTag,
sub.RepoURL,
)
}
// allows returns true if the given tag name matches the given regular
// expression or if the regular expression is nil. It returns false otherwise.
func allows(tagName string, allowRegex *regexp.Regexp) bool {
if allowRegex == nil {
return true
}
return allowRegex.MatchString(tagName)
}
// ignores returns true if the given tag name is in the given list of ignored
// tag names. It returns false otherwise.
func ignores(tagName string, ignore []string) bool {
for _, i := range ignore {
if i == tagName {
return true
}
}
return false
}
// selectLexicallyLastTag sorts the provided tag name in reverse lexicographic
// order and returns the first tag name in the sorted list. If the list is
// empty, it returns an empty string.
func selectLexicallyLastTag(tagNames []string) string {
if len(tagNames) == 0 {
return ""
}
sort.Slice(tagNames, func(i, j int) bool {
return tagNames[i] > tagNames[j]
})
return tagNames[0]
}
// selectSemverTag narrows the provided list of tag names to those that are
// valid semantic versions. If constraintStr is non-empty, it further narrows
// the list to those that satisfy the constraint. If the narrowed list is
// non-empty, it sorts the list in reverse semver order and returns the first
// tag name in the sorted list. If the narrowed list is empty, it returns an
// empty string.
func selectSemverTag(tagNames []string, constraintStr string) (string, error) {
var constraint *semver.Constraints
if constraintStr != "" {
var err error
if constraint, err = semver.NewConstraint(constraintStr); err != nil {
return "", errors.Wrapf(
err,
"error parsing semver constraint %q",
constraintStr,
)
}
}
semvers := make([]*semver.Version, 0, len(tagNames))
for _, tagName := range tagNames {
sv, err := semver.NewVersion(tagName)
if err != nil {
continue // tagName wasn't a semantic version
}
if constraint == nil || constraint.Check(sv) {
semvers = append(semvers, sv)
}
}
if len(semvers) == 0 {
return "", nil
}
sort.Slice(semvers, func(i, j int) bool {
if comp := semvers[i].Compare(semvers[j]); comp != 0 {
return comp > 0
}
// If the semvers tie, break the tie lexically using the original strings
// used to construct the semvers. This ensures a deterministic comparison
// of equivalent semvers, e.g., 1.0 and 1.0.0.
return semvers[i].Original() > semvers[j].Original()
})
return semvers[0].Original(), nil
}
func (r *reconciler) getLastCommitID(repo git.Repo) (string, error) {
return repo.LastCommitID()
}
func (r *reconciler) listTags(repo git.Repo) ([]string, error) {
return repo.ListTags()
}
func (r *reconciler) checkoutTag(repo git.Repo, tag string) error {
return repo.Checkout(tag)
}