Skip to content

Commit 6c5331b

Browse files
committed
pkg/git: introduce concrete and partial commit
Introduce concrete and partial commits. Concrete commits have all the information from remote including the hash and commit content. Partial commits are based on locally available copy of a repo, they may only contain the commit hash and reference. IsConcreteCommit() can be used to find out if a given commit is based on local information or full remote repo information. Update go-git and libgit2 branch/tag clone optimization to return a partial commit and no error. Update and simplify the go-git and libgit2 tests for the same. Signed-off-by: Sunny <[email protected]>
1 parent 4f11dd9 commit 6c5331b

File tree

7 files changed

+391
-225
lines changed

7 files changed

+391
-225
lines changed

pkg/git/git.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,13 @@ type NoChangesError struct {
118118
func (e NoChangesError) Error() string {
119119
return fmt.Sprintf("%s: observed revision '%s'", e.Message, e.ObservedRevision)
120120
}
121+
122+
// IsConcreteCommit returns if a given commit is a concrete commit. Concrete
123+
// commits have most of commit metadata and commit content. In contrast, a
124+
// partial commit may only have some metadata and no commit content.
125+
func IsConcreteCommit(c Commit) bool {
126+
if c.Hash != nil && c.Encoded != nil {
127+
return true
128+
}
129+
return false
130+
}

pkg/git/git_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package git
1818

1919
import (
2020
"testing"
21+
"time"
2122

2223
. "github.com/onsi/gomega"
2324
)
@@ -263,3 +264,41 @@ of the commit`,
263264
})
264265
}
265266
}
267+
268+
func TestIsConcreteCommit(t *testing.T) {
269+
tests := []struct {
270+
name string
271+
commit Commit
272+
result bool
273+
}{
274+
{
275+
name: "concrete commit",
276+
commit: Commit{
277+
Hash: Hash("foo"),
278+
Reference: "refs/tags/main",
279+
Author: Signature{
280+
Name: "user", Email: "[email protected]", When: time.Now(),
281+
},
282+
Committer: Signature{
283+
Name: "user", Email: "[email protected]", When: time.Now(),
284+
},
285+
Signature: "signature",
286+
Encoded: []byte("commit-content"),
287+
Message: "commit-message",
288+
},
289+
result: true,
290+
},
291+
{
292+
name: "partial commit",
293+
commit: Commit{Hash: Hash("foo")},
294+
result: false,
295+
},
296+
}
297+
298+
for _, tt := range tests {
299+
t.Run(tt.name, func(t *testing.T) {
300+
g := NewWithT(t)
301+
g.Expect(IsConcreteCommit(tt.commit)).To(Equal(tt.result))
302+
})
303+
}
304+
}

pkg/git/gogit/checkout.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"fmt"
2323
"io"
2424
"sort"
25+
"strings"
2526
"time"
2627

2728
"github.com/Masterminds/semver/v3"
@@ -78,10 +79,21 @@ func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *g
7879
}
7980

8081
if currentRevision != "" && currentRevision == c.LastRevision {
81-
return nil, git.NoChangesError{
82-
Message: "no changes since last reconcilation",
83-
ObservedRevision: currentRevision,
82+
// Construct a partial commit with the existing information.
83+
// Split the revision and take the last part as the hash.
84+
// Example revision: main/43d7eb9c49cdd49b2494efd481aea1166fc22b67
85+
var hash git.Hash
86+
ss := strings.Split(currentRevision, "/")
87+
if len(ss) > 1 {
88+
hash = git.Hash(ss[len(ss)-1])
89+
} else {
90+
hash = git.Hash(ss[0])
8491
}
92+
c := &git.Commit{
93+
Hash: hash,
94+
Reference: plumbing.NewBranchReferenceName(c.Branch).String(),
95+
}
96+
return c, nil
8597
}
8698
}
8799

@@ -153,10 +165,21 @@ func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git.
153165
}
154166

155167
if currentRevision != "" && currentRevision == c.LastRevision {
156-
return nil, git.NoChangesError{
157-
Message: "no changes since last reconcilation",
158-
ObservedRevision: currentRevision,
168+
// Construct a partial commit with the existing information.
169+
// Split the revision and take the last part as the hash.
170+
// Example revision: 6.1.4/bf09377bfd5d3bcac1e895fa8ce52dc76695c060
171+
var hash git.Hash
172+
ss := strings.Split(currentRevision, "/")
173+
if len(ss) > 1 {
174+
hash = git.Hash(ss[len(ss)-1])
175+
} else {
176+
hash = git.Hash(ss[0])
177+
}
178+
c := &git.Commit{
179+
Hash: hash,
180+
Reference: ref.String(),
159181
}
182+
return c, nil
160183
}
161184
}
162185
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{

pkg/git/gogit/checkout_test.go

Lines changed: 96 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -67,32 +67,36 @@ func TestCheckoutBranch_Checkout(t *testing.T) {
6767
}
6868

6969
tests := []struct {
70-
name string
71-
branch string
72-
filesCreated map[string]string
73-
expectedCommit string
74-
expectedErr string
75-
lastRevision string
70+
name string
71+
branch string
72+
filesCreated map[string]string
73+
lastRevision string
74+
expectedCommit string
75+
expectedConcreteCommit bool
76+
expectedErr string
7677
}{
7778
{
78-
name: "Default branch",
79-
branch: "master",
80-
filesCreated: map[string]string{"branch": "init"},
81-
expectedCommit: firstCommit.String(),
79+
name: "Default branch",
80+
branch: "master",
81+
filesCreated: map[string]string{"branch": "init"},
82+
expectedCommit: firstCommit.String(),
83+
expectedConcreteCommit: true,
8284
},
8385
{
84-
name: "skip clone if LastRevision hasn't changed",
85-
branch: "master",
86-
filesCreated: map[string]string{"branch": "init"},
87-
expectedErr: fmt.Sprintf("no changes since last reconcilation: observed revision 'master/%s'", firstCommit.String()),
88-
lastRevision: fmt.Sprintf("master/%s", firstCommit.String()),
86+
name: "skip clone if LastRevision hasn't changed",
87+
branch: "master",
88+
filesCreated: map[string]string{"branch": "init"},
89+
lastRevision: fmt.Sprintf("master/%s", firstCommit.String()),
90+
expectedCommit: firstCommit.String(),
91+
expectedConcreteCommit: false,
8992
},
9093
{
91-
name: "Other branch - revision has changed",
92-
branch: "test",
93-
filesCreated: map[string]string{"branch": "second"},
94-
expectedCommit: secondCommit.String(),
95-
lastRevision: fmt.Sprintf("master/%s", firstCommit.String()),
94+
name: "Other branch - revision has changed",
95+
branch: "test",
96+
filesCreated: map[string]string{"branch": "second"},
97+
lastRevision: fmt.Sprintf("master/%s", firstCommit.String()),
98+
expectedCommit: secondCommit.String(),
99+
expectedConcreteCommit: true,
96100
},
97101
{
98102
name: "Non existing branch",
@@ -120,58 +124,64 @@ func TestCheckoutBranch_Checkout(t *testing.T) {
120124
}
121125
g.Expect(err).ToNot(HaveOccurred())
122126
g.Expect(cc.String()).To(Equal(tt.branch + "/" + tt.expectedCommit))
127+
g.Expect(git.IsConcreteCommit(*cc)).To(Equal(tt.expectedConcreteCommit))
123128

124-
for k, v := range tt.filesCreated {
125-
g.Expect(filepath.Join(tmpDir, k)).To(BeARegularFile())
126-
g.Expect(os.ReadFile(filepath.Join(tmpDir, k))).To(BeEquivalentTo(v))
129+
if tt.expectedConcreteCommit {
130+
for k, v := range tt.filesCreated {
131+
g.Expect(filepath.Join(tmpDir, k)).To(BeARegularFile())
132+
g.Expect(os.ReadFile(filepath.Join(tmpDir, k))).To(BeEquivalentTo(v))
133+
}
127134
}
128135
})
129136
}
130137
}
131138

132139
func TestCheckoutTag_Checkout(t *testing.T) {
140+
type testTag struct {
141+
name string
142+
annotated bool
143+
}
144+
133145
tests := []struct {
134-
name string
135-
tag string
136-
annotated bool
137-
checkoutTag string
138-
expectTag string
139-
expectErr string
140-
lastRev string
141-
setLastRev bool
146+
name string
147+
tagsInRepo []testTag
148+
checkoutTag string
149+
lastRevTag string
150+
expectConcreteCommit bool
151+
expectErr string
142152
}{
143153
{
144-
name: "Tag",
145-
tag: "tag-1",
146-
checkoutTag: "tag-1",
147-
expectTag: "tag-1",
154+
name: "Tag",
155+
tagsInRepo: []testTag{{"tag-1", false}},
156+
checkoutTag: "tag-1",
157+
expectConcreteCommit: true,
148158
},
149159
{
150-
name: "Skip Tag if last revision hasn't changed",
151-
tag: "tag-2",
152-
checkoutTag: "tag-2",
153-
setLastRev: true,
154-
expectErr: "no changes since last reconcilation",
160+
name: "Annotated",
161+
tagsInRepo: []testTag{{"annotated", true}},
162+
checkoutTag: "annotated",
163+
expectConcreteCommit: true,
155164
},
156165
{
157-
name: "Last revision changed",
158-
tag: "tag-3",
159-
checkoutTag: "tag-3",
160-
expectTag: "tag-3",
161-
lastRev: "tag-3/<fake-hash>",
166+
name: "Non existing tag",
167+
// Without this go-git returns error "remote repository is empty".
168+
tagsInRepo: []testTag{{"tag-1", false}},
169+
checkoutTag: "invalid",
170+
expectErr: "couldn't find remote ref \"refs/tags/invalid\"",
162171
},
163172
{
164-
name: "Annotated",
165-
tag: "annotated",
166-
annotated: true,
167-
checkoutTag: "annotated",
168-
expectTag: "annotated",
173+
name: "Skip clone - last revision unchanged",
174+
tagsInRepo: []testTag{{"tag-1", false}},
175+
checkoutTag: "tag-1",
176+
lastRevTag: "tag-1",
177+
expectConcreteCommit: false,
169178
},
170179
{
171-
name: "Non existing tag",
172-
tag: "tag-1",
173-
checkoutTag: "invalid",
174-
expectErr: "couldn't find remote ref \"refs/tags/invalid\"",
180+
name: "Last revision changed",
181+
tagsInRepo: []testTag{{"tag-1", false}, {"tag-2", false}},
182+
checkoutTag: "tag-2",
183+
lastRevTag: "tag-1",
184+
expectConcreteCommit: true,
175185
},
176186
}
177187
for _, tt := range tests {
@@ -183,43 +193,55 @@ func TestCheckoutTag_Checkout(t *testing.T) {
183193
t.Fatal(err)
184194
}
185195

186-
var h plumbing.Hash
187-
var tagHash *plumbing.Reference
188-
if tt.tag != "" {
189-
h, err = commitFile(repo, "tag", tt.tag, time.Now())
190-
if err != nil {
191-
t.Fatal(err)
192-
}
193-
tagHash, err = tag(repo, h, !tt.annotated, tt.tag, time.Now())
194-
if err != nil {
195-
t.Fatal(err)
196+
// Collect tags and their associated commit hash for later
197+
// reference.
198+
tagCommits := map[string]string{}
199+
200+
// Populate the repo with commits and tags.
201+
if tt.tagsInRepo != nil {
202+
for _, tr := range tt.tagsInRepo {
203+
h, err := commitFile(repo, "tag", tr.name, time.Now())
204+
if err != nil {
205+
t.Fatal(err)
206+
}
207+
_, err = tag(repo, h, tr.annotated, tr.name, time.Now())
208+
if err != nil {
209+
t.Fatal(err)
210+
}
211+
tagCommits[tr.name] = h.String()
196212
}
197213
}
198214

199-
tag := CheckoutTag{
215+
checkoutTag := CheckoutTag{
200216
Tag: tt.checkoutTag,
201217
}
202-
if tt.setLastRev {
203-
tag.LastRevision = fmt.Sprintf("%s/%s", tt.tag, tagHash.Hash().String())
218+
// If last revision is provided, configure it.
219+
if tt.lastRevTag != "" {
220+
lc := tagCommits[tt.lastRevTag]
221+
checkoutTag.LastRevision = fmt.Sprintf("%s/%s", tt.lastRevTag, lc)
204222
}
205223

206-
if tt.lastRev != "" {
207-
tag.LastRevision = tt.lastRev
208-
}
209224
tmpDir := t.TempDir()
210225

211-
cc, err := tag.Checkout(context.TODO(), tmpDir, path, nil)
226+
cc, err := checkoutTag.Checkout(context.TODO(), tmpDir, path, nil)
212227
if tt.expectErr != "" {
213228
g.Expect(err).ToNot(BeNil())
214229
g.Expect(err.Error()).To(ContainSubstring(tt.expectErr))
215230
g.Expect(cc).To(BeNil())
216231
return
217232
}
218233

234+
// Check successful checkout results.
235+
g.Expect(git.IsConcreteCommit(*cc)).To(Equal(tt.expectConcreteCommit))
236+
targetTagHash := tagCommits[tt.checkoutTag]
219237
g.Expect(err).ToNot(HaveOccurred())
220-
g.Expect(cc.String()).To(Equal(tt.expectTag + "/" + h.String()))
221-
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
222-
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.tag))
238+
g.Expect(cc.String()).To(Equal(tt.checkoutTag + "/" + targetTagHash))
239+
240+
// Check file content only when there's an actual checkout.
241+
if tt.lastRevTag != tt.checkoutTag {
242+
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
243+
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.checkoutTag))
244+
}
223245
})
224246
}
225247
}

0 commit comments

Comments
 (0)