Skip to content

Commit b484454

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 <hello@hidde.co>
1 parent 79c19ad commit b484454

File tree

2 files changed

+199
-43
lines changed

2 files changed

+199
-43
lines changed

pkg/git/libgit2/checkout.go

+20-11
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ import (
2020
"context"
2121
"fmt"
2222
"sort"
23+
"strings"
2324
"time"
2425

2526
"github.com/Masterminds/semver/v3"
27+
"github.com/fluxcd/pkg/gitutil"
2628
"github.com/fluxcd/pkg/version"
2729
git2go "github.com/libgit2/git2go/v31"
2830

29-
"github.com/fluxcd/pkg/gitutil"
30-
3131
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
3232
"github.com/fluxcd/source-controller/pkg/git"
3333
)
@@ -115,7 +115,7 @@ func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, auth *git.
115115
if err != nil {
116116
return nil, "", fmt.Errorf("git commit '%s' not found: %w", head.Target(), err)
117117
}
118-
err = repo.CheckoutHead(&git2go.CheckoutOpts{
118+
err = repo.CheckoutHead(&git2go.CheckoutOptions{
119119
Strategy: git2go.CheckoutForce,
120120
})
121121
if err != nil {
@@ -192,21 +192,30 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *g
192192
tags := make(map[string]string)
193193
tagTimestamps := make(map[string]time.Time)
194194
if err := repo.Tags.Foreach(func(name string, id *git2go.Oid) error {
195-
tag, err := repo.LookupTag(id)
196-
if err != nil {
195+
// The given ID can refer to both a commit and a tag, as annotated tags contain additional metadata.
196+
// Due to this, first attempt to resolve it as a simple tag (commit), but fallback to attempting to
197+
// resolve it as an annotated tag in case this results in an error.
198+
if c, err := repo.LookupCommit(id); err == nil {
199+
cleanName := strings.TrimPrefix(name, "refs/tags/")
200+
// Use the commit metadata as the decisive timestamp.
201+
tagTimestamps[cleanName] = c.Committer().When
202+
tags[cleanName] = name
197203
return nil
198204
}
199-
200-
commit, err := tag.Peel(git2go.ObjectCommit)
205+
t, err := repo.LookupTag(id)
206+
if err != nil {
207+
return fmt.Errorf("could not lookup tag for Git object id '%s' for tag '%s': %w", name, id.String(), err)
208+
}
209+
commit, err := t.Peel(git2go.ObjectCommit)
201210
if err != nil {
202-
return fmt.Errorf("can't get commit for tag %s: %w", name, err)
211+
return fmt.Errorf("could not get commit for tag '%s': %w", name, err)
203212
}
204213
c, err := commit.AsCommit()
205214
if err != nil {
206-
return err
215+
return fmt.Errorf("could not get commit object for tag '%s': %w", name, err)
207216
}
208-
tagTimestamps[tag.Name()] = c.Committer().When
209-
tags[tag.Name()] = name
217+
tagTimestamps[t.Name()] = c.Committer().When
218+
tags[t.Name()] = name
210219
return nil
211220
}); err != nil {
212221
return nil, "", err

pkg/git/libgit2/checkout_test.go

+179-32
Original file line numberDiff line numberDiff line change
@@ -18,63 +18,210 @@ package libgit2
1818

1919
import (
2020
"context"
21-
"crypto/sha256"
22-
"encoding/hex"
23-
"io"
21+
"errors"
22+
"fmt"
2423
"os"
25-
"path"
2624
"testing"
25+
"time"
2726

2827
git2go "github.com/libgit2/git2go/v31"
28+
. "github.com/onsi/gomega"
2929

3030
"github.com/fluxcd/source-controller/pkg/git"
3131
)
3232

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

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

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

50-
// Ensure the correct files are checked out on disk
51-
f, err := os.Open(path.Join(tmpDir, "README.md"))
173+
blobOID, err := repo.CreateBlobFromBuffer([]byte(content))
52174
if err != nil {
53-
t.Error(err)
175+
return nil, err
176+
}
177+
178+
entry := &git2go.IndexEntry{
179+
Mode: git2go.FilemodeBlob,
180+
Id: blobOID,
181+
Path: path,
182+
}
183+
184+
if err := index.Add(entry); err != nil {
185+
return nil, err
54186
}
55-
defer f.Close()
56-
h := sha256.New()
57-
if _, err := io.Copy(h, f); err != nil {
58-
t.Error(err)
187+
if err := index.Write(); err != nil {
188+
return nil, err
59189
}
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)
190+
191+
newTreeOID, err := index.WriteTree()
192+
if err != nil {
193+
return nil, err
194+
}
195+
196+
tree, err := repo.LookupTree(newTreeOID)
197+
if err != nil {
198+
return nil, err
64199
}
200+
defer tree.Free()
65201

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

72-
cSemVer, _, err := semVer.Checkout(context.TODO(), tmpDir2, "https://github.com/projectcontour/contour", auth)
207+
return commit, nil
208+
}
209+
210+
func tag(repo *git2go.Repository, cId *git2go.Oid, simple bool, tag string, time time.Time) (*git2go.Oid, error) {
211+
commit, err := repo.LookupCommit(cId)
73212
if err != nil {
74-
t.Error(err)
213+
return nil, err
75214
}
215+
if simple {
216+
return repo.Tags.CreateLightweight(tag, commit, false)
217+
}
218+
return repo.Tags.Create(tag, commit, signature(time), fmt.Sprintf("Annotated tag for %s", tag))
219+
}
76220

77-
if cTag.Hash() != cSemVer.Hash() {
78-
t.Errorf("expected semver hash %s, got %s", cTag.Hash(), cSemVer.Hash())
221+
func signature(time time.Time) *git2go.Signature {
222+
return &git2go.Signature{
223+
Name: "Jane Doe",
224+
Email: "author@example.com",
225+
When: time,
79226
}
80227
}

0 commit comments

Comments
 (0)