Skip to content

Commit e736493

Browse files
committed
libgit2: correctly resolve (annotated) tags
In d0560e5 the SemVer implementations were aligned, and the logic was simplified a bit (or so I thought). This did however result in the introduction of a regression, as it failed to take "simple tags" into account. This commit ensures both are taken into account again, and ensures it is now covered by a proper test. Signed-off-by: Hidde Beydals <[email protected]>
1 parent 79c19ad commit e736493

File tree

2 files changed

+205
-44
lines changed

2 files changed

+205
-44
lines changed

pkg/git/libgit2/checkout.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ import (
2020
"context"
2121
"fmt"
2222
"sort"
23+
"strings"
2324
"time"
2425

2526
"github.com/Masterminds/semver/v3"
26-
"github.com/fluxcd/pkg/version"
2727
git2go "github.com/libgit2/git2go/v31"
2828

2929
"github.com/fluxcd/pkg/gitutil"
30+
"github.com/fluxcd/pkg/version"
3031

3132
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
3233
"github.com/fluxcd/source-controller/pkg/git"
@@ -115,7 +116,7 @@ func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, auth *git.
115116
if err != nil {
116117
return nil, "", fmt.Errorf("git commit '%s' not found: %w", head.Target(), err)
117118
}
118-
err = repo.CheckoutHead(&git2go.CheckoutOpts{
119+
err = repo.CheckoutHead(&git2go.CheckoutOptions{
119120
Strategy: git2go.CheckoutForce,
120121
})
121122
if err != nil {
@@ -192,28 +193,37 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *g
192193
tags := make(map[string]string)
193194
tagTimestamps := make(map[string]time.Time)
194195
if err := repo.Tags.Foreach(func(name string, id *git2go.Oid) error {
195-
tag, err := repo.LookupTag(id)
196-
if err != nil {
196+
cleanName := strings.TrimPrefix(name, "refs/tags/")
197+
// The given ID can refer to both a commit and a tag, as annotated tags contain additional metadata.
198+
// Due to this, first attempt to resolve it as a simple tag (commit), but fallback to attempting to
199+
// resolve it as an annotated tag in case this results in an error.
200+
if c, err := repo.LookupCommit(id); err == nil {
201+
// Use the commit metadata as the decisive timestamp.
202+
tagTimestamps[cleanName] = c.Committer().When
203+
tags[cleanName] = name
197204
return nil
198205
}
199-
200-
commit, err := tag.Peel(git2go.ObjectCommit)
206+
t, err := repo.LookupTag(id)
207+
if err != nil {
208+
return fmt.Errorf("could not lookup '%s' as simple or annotated tag: %w", cleanName, err)
209+
}
210+
commit, err := t.Peel(git2go.ObjectCommit)
201211
if err != nil {
202-
return fmt.Errorf("can't get commit for tag %s: %w", name, err)
212+
return fmt.Errorf("could not get commit for tag '%s': %w", t.Name(), err)
203213
}
204214
c, err := commit.AsCommit()
205215
if err != nil {
206-
return err
216+
return fmt.Errorf("could not get commit object for tag '%s': %w", t.Name(), err)
207217
}
208-
tagTimestamps[tag.Name()] = c.Committer().When
209-
tags[tag.Name()] = name
218+
tagTimestamps[t.Name()] = c.Committer().When
219+
tags[t.Name()] = name
210220
return nil
211221
}); err != nil {
212222
return nil, "", err
213223
}
214224

215225
var matchedVersions semver.Collection
216-
for tag, _ := range tags {
226+
for tag := range tags {
217227
v, err := version.ParseVersion(tag)
218228
if err != nil {
219229
continue
@@ -261,7 +271,7 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *g
261271
if err != nil {
262272
return nil, "", fmt.Errorf("git commit '%s' not found: %w", head.Target().String(), err)
263273
}
264-
err = repo.CheckoutHead(&git2go.CheckoutOpts{
274+
err = repo.CheckoutHead(&git2go.CheckoutOptions{
265275
Strategy: git2go.CheckoutForce,
266276
})
267277
if err != nil {

pkg/git/libgit2/checkout_test.go

Lines changed: 183 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,63 +18,214 @@ package libgit2
1818

1919
import (
2020
"context"
21-
"crypto/sha256"
22-
"encoding/hex"
23-
"io"
21+
"errors"
22+
"fmt"
2423
"os"
25-
"path"
24+
"path/filepath"
2625
"testing"
26+
"time"
2727

2828
git2go "github.com/libgit2/git2go/v31"
29+
. "github.com/onsi/gomega"
2930

3031
"github.com/fluxcd/source-controller/pkg/git"
3132
)
3233

3334
func TestCheckoutTagSemVer_Checkout(t *testing.T) {
34-
certCallback := func(cert *git2go.Certificate, valid bool, hostname string) git2go.ErrorCode {
35-
return git2go.ErrorCodeOK
35+
g := NewWithT(t)
36+
now := time.Now()
37+
38+
tags := []struct{
39+
tag string
40+
simple bool
41+
commitTime time.Time
42+
tagTime time.Time
43+
}{
44+
{
45+
tag: "v0.0.1",
46+
simple: true,
47+
commitTime: now,
48+
},
49+
{
50+
tag: "v0.1.0+build-1",
51+
simple: false,
52+
commitTime: now.Add(1 * time.Minute),
53+
tagTime: now.Add(1 * time.Hour), // This should be ignored during TS comparisons
54+
},
55+
{
56+
tag: "v0.1.0+build-2",
57+
simple: true,
58+
commitTime: now.Add(2 * time.Minute),
59+
},
60+
{
61+
tag: "0.2.0",
62+
simple: false,
63+
commitTime: now,
64+
tagTime: now,
65+
},
66+
}
67+
tests := []struct{
68+
name string
69+
constraint string
70+
expectError error
71+
expectTag string
72+
}{
73+
{
74+
name: "Orders by SemVer",
75+
constraint: ">0.1.0",
76+
expectTag: "0.2.0",
77+
},
78+
{
79+
name: "Orders by SemVer and timestamp",
80+
constraint: "<0.2.0",
81+
expectTag: "v0.1.0+build-2",
82+
},
83+
{
84+
name: "Errors without match",
85+
constraint: ">=1.0.0",
86+
expectError: errors.New("no match found for semver: >=1.0.0"),
87+
},
88+
}
89+
90+
repo, err := initBareRepo()
91+
if err != nil {
92+
t.Fatal(err)
93+
}
94+
defer repo.Free()
95+
defer os.RemoveAll(repo.Path())
96+
97+
for _, tt := range tags {
98+
cId, err := commit(repo, "tag.txt", tt.tag, tt.commitTime)
99+
if err != nil {
100+
t.Fatal(err)
101+
}
102+
_, err = tag(repo, cId, tt.simple, tt.tag, tt.tagTime)
103+
if err != nil {
104+
t.Fatal(err)
105+
}
106+
}
107+
108+
c, err := repo.Tags.List()
109+
g.Expect(err).ToNot(HaveOccurred())
110+
g.Expect(c).To(HaveLen(len(tags)))
111+
112+
for _, tt := range tests {
113+
t.Run(tt.name, func(t *testing.T) {
114+
semVer := CheckoutSemVer{
115+
semVer: tt.constraint,
116+
}
117+
tmpDir, _ := os.MkdirTemp("", "test")
118+
defer os.RemoveAll(tmpDir)
119+
120+
_, ref, err := semVer.Checkout(context.TODO(), tmpDir, repo.Path(), &git.Auth{})
121+
if tt.expectError != nil {
122+
g.Expect(err).To(Equal(tt.expectError))
123+
g.Expect(ref).To(BeEmpty())
124+
return
125+
}
126+
g.Expect(err).ToNot(HaveOccurred())
127+
g.Expect(ref).To(HavePrefix(tt.expectTag + "/"))
128+
content, err := os.ReadFile(filepath.Join(tmpDir, "tag.txt"))
129+
g.Expect(err).ToNot(HaveOccurred())
130+
g.Expect(content).To(BeEquivalentTo(tt.expectTag))
131+
})
132+
}
133+
}
134+
135+
func initBareRepo() (*git2go.Repository, error) {
136+
tmpDir, err := os.MkdirTemp("", "git2go-")
137+
if err != nil {
138+
return nil, err
139+
}
140+
repo, err := git2go.InitRepository(tmpDir, false)
141+
if err != nil {
142+
_ = os.RemoveAll(tmpDir)
143+
return nil, err
144+
}
145+
return repo, nil
146+
}
147+
148+
func headCommit(repo *git2go.Repository) (*git2go.Commit, error) {
149+
head, err := repo.Head()
150+
if err != nil {
151+
return nil, err
36152
}
37-
auth := &git.Auth{CertCallback: certCallback}
153+
defer head.Free()
154+
155+
commit, err := repo.LookupCommit(head.Target())
156+
if err != nil {
157+
return nil, err
158+
}
159+
160+
return commit, nil
161+
}
38162

39-
tag := CheckoutTag{
40-
tag: "v1.7.0",
163+
func commit(repo *git2go.Repository, path, content string, time time.Time) (*git2go.Oid, error) {
164+
var parentC []*git2go.Commit
165+
head, err := headCommit(repo)
166+
if err == nil {
167+
defer head.Free()
168+
parentC = append(parentC, head)
41169
}
42-
tmpDir, _ := os.MkdirTemp("", "test")
43-
defer os.RemoveAll(tmpDir)
44170

45-
cTag, _, err := tag.Checkout(context.TODO(), tmpDir, "https://github.com/projectcontour/contour", auth)
171+
index, err := repo.Index()
46172
if err != nil {
47-
t.Error(err)
173+
return nil, err
48174
}
175+
defer index.Free()
49176

50-
// Ensure the correct files are checked out on disk
51-
f, err := os.Open(path.Join(tmpDir, "README.md"))
177+
blobOID, err := repo.CreateBlobFromBuffer([]byte(content))
52178
if err != nil {
53-
t.Error(err)
179+
return nil, err
180+
}
181+
182+
entry := &git2go.IndexEntry{
183+
Mode: git2go.FilemodeBlob,
184+
Id: blobOID,
185+
Path: path,
186+
}
187+
188+
if err := index.Add(entry); err != nil {
189+
return nil, err
54190
}
55-
defer f.Close()
56-
h := sha256.New()
57-
if _, err := io.Copy(h, f); err != nil {
58-
t.Error(err)
191+
if err := index.Write(); err != nil {
192+
return nil, err
59193
}
60-
const expectedHash = "2bd1707542a11f987ee24698dcc095a9f57639f401133ef6a29da97bf8f3f302"
61-
fileHash := hex.EncodeToString(h.Sum(nil))
62-
if fileHash != expectedHash {
63-
t.Errorf("expected files not checked out. Expected hash %s, got %s", expectedHash, fileHash)
194+
195+
newTreeOID, err := index.WriteTree()
196+
if err != nil {
197+
return nil, err
198+
}
199+
200+
tree, err := repo.LookupTree(newTreeOID)
201+
if err != nil {
202+
return nil, err
64203
}
204+
defer tree.Free()
65205

66-
semVer := CheckoutSemVer{
67-
semVer: ">=1.0.0 <=1.7.0",
206+
commit, err := repo.CreateCommit("HEAD", signature(time), signature(time), "Committing "+path, tree, parentC...)
207+
if err != nil {
208+
return nil, err
68209
}
69-
tmpDir2, _ := os.MkdirTemp("", "test")
70-
defer os.RemoveAll(tmpDir2)
71210

72-
cSemVer, _, err := semVer.Checkout(context.TODO(), tmpDir2, "https://github.com/projectcontour/contour", auth)
211+
return commit, nil
212+
}
213+
214+
func tag(repo *git2go.Repository, cId *git2go.Oid, simple bool, tag string, time time.Time) (*git2go.Oid, error) {
215+
commit, err := repo.LookupCommit(cId)
73216
if err != nil {
74-
t.Error(err)
217+
return nil, err
75218
}
219+
if simple {
220+
return repo.Tags.CreateLightweight(tag, commit, false)
221+
}
222+
return repo.Tags.Create(tag, commit, signature(time), fmt.Sprintf("Annotated tag for %s", tag))
223+
}
76224

77-
if cTag.Hash() != cSemVer.Hash() {
78-
t.Errorf("expected semver hash %s, got %s", cTag.Hash(), cSemVer.Hash())
225+
func signature(time time.Time) *git2go.Signature {
226+
return &git2go.Signature{
227+
Name: "Jane Doe",
228+
229+
When: time,
79230
}
80231
}

0 commit comments

Comments
 (0)