Skip to content

Commit 2393437

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 9651950 commit 2393437

File tree

7 files changed

+282
-194
lines changed

7 files changed

+282
-194
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: 17 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,15 @@ 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+
ss := strings.Split(currentRevision, "/")
86+
c := &git.Commit{
87+
Hash: git.Hash(ss[len(ss)-1]),
88+
Reference: ref.String(),
8489
}
90+
return c, nil
8591
}
8692
}
8793

@@ -153,10 +159,15 @@ func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git.
153159
}
154160

155161
if currentRevision != "" && currentRevision == c.LastRevision {
156-
return nil, git.NoChangesError{
157-
Message: "no changes since last reconcilation",
158-
ObservedRevision: currentRevision,
162+
// Construct a partial commit with the existing information.
163+
// Split the revision and take the last part as the hash.
164+
// Example revision: 6.1.4/bf09377bfd5d3bcac1e895fa8ce52dc76695c060
165+
ss := strings.Split(currentRevision, "/")
166+
c := &git.Commit{
167+
Hash: git.Hash(ss[len(ss)-1]),
168+
Reference: ref.String(),
159169
}
170+
return c, nil
160171
}
161172
}
162173
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{

pkg/git/gogit/checkout_test.go

Lines changed: 97 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,65 @@ 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+
// Check checked out content for actual checkouts only.
130+
if tt.expectedConcreteCommit {
131+
for k, v := range tt.filesCreated {
132+
g.Expect(filepath.Join(tmpDir, k)).To(BeARegularFile())
133+
g.Expect(os.ReadFile(filepath.Join(tmpDir, k))).To(BeEquivalentTo(v))
134+
}
127135
}
128136
})
129137
}
130138
}
131139

132140
func TestCheckoutTag_Checkout(t *testing.T) {
141+
type testTag struct {
142+
name string
143+
annotated bool
144+
}
145+
133146
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
147+
name string
148+
tagsInRepo []testTag
149+
checkoutTag string
150+
lastRevTag string
151+
expectConcreteCommit bool
152+
expectErr string
142153
}{
143154
{
144-
name: "Tag",
145-
tag: "tag-1",
146-
checkoutTag: "tag-1",
147-
expectTag: "tag-1",
155+
name: "Tag",
156+
tagsInRepo: []testTag{{"tag-1", false}},
157+
checkoutTag: "tag-1",
158+
expectConcreteCommit: true,
148159
},
149160
{
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",
161+
name: "Annotated",
162+
tagsInRepo: []testTag{{"annotated", true}},
163+
checkoutTag: "annotated",
164+
expectConcreteCommit: true,
155165
},
156166
{
157-
name: "Last revision changed",
158-
tag: "tag-3",
159-
checkoutTag: "tag-3",
160-
expectTag: "tag-3",
161-
lastRev: "tag-3/<fake-hash>",
167+
name: "Non existing tag",
168+
// Without this go-git returns error "remote repository is empty".
169+
tagsInRepo: []testTag{{"tag-1", false}},
170+
checkoutTag: "invalid",
171+
expectErr: "couldn't find remote ref \"refs/tags/invalid\"",
162172
},
163173
{
164-
name: "Annotated",
165-
tag: "annotated",
166-
annotated: true,
167-
checkoutTag: "annotated",
168-
expectTag: "annotated",
174+
name: "Skip clone - last revision unchanged",
175+
tagsInRepo: []testTag{{"tag-1", false}},
176+
checkoutTag: "tag-1",
177+
lastRevTag: "tag-1",
178+
expectConcreteCommit: false,
169179
},
170180
{
171-
name: "Non existing tag",
172-
tag: "tag-1",
173-
checkoutTag: "invalid",
174-
expectErr: "couldn't find remote ref \"refs/tags/invalid\"",
181+
name: "Last revision changed",
182+
tagsInRepo: []testTag{{"tag-1", false}, {"tag-2", false}},
183+
checkoutTag: "tag-2",
184+
lastRevTag: "tag-1",
185+
expectConcreteCommit: true,
175186
},
176187
}
177188
for _, tt := range tests {
@@ -183,43 +194,55 @@ func TestCheckoutTag_Checkout(t *testing.T) {
183194
t.Fatal(err)
184195
}
185196

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)
197+
// Collect tags and their associated commit hash for later
198+
// reference.
199+
tagCommits := map[string]string{}
200+
201+
// Populate the repo with commits and tags.
202+
if tt.tagsInRepo != nil {
203+
for _, tr := range tt.tagsInRepo {
204+
h, err := commitFile(repo, "tag", tr.name, time.Now())
205+
if err != nil {
206+
t.Fatal(err)
207+
}
208+
_, err = tag(repo, h, tr.annotated, tr.name, time.Now())
209+
if err != nil {
210+
t.Fatal(err)
211+
}
212+
tagCommits[tr.name] = h.String()
196213
}
197214
}
198215

199-
tag := CheckoutTag{
216+
checkoutTag := CheckoutTag{
200217
Tag: tt.checkoutTag,
201218
}
202-
if tt.setLastRev {
203-
tag.LastRevision = fmt.Sprintf("%s/%s", tt.tag, tagHash.Hash().String())
219+
// If last revision is provided, configure it.
220+
if tt.lastRevTag != "" {
221+
lc := tagCommits[tt.lastRevTag]
222+
checkoutTag.LastRevision = fmt.Sprintf("%s/%s", tt.lastRevTag, lc)
204223
}
205224

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

211-
cc, err := tag.Checkout(context.TODO(), tmpDir, path, nil)
227+
cc, err := checkoutTag.Checkout(context.TODO(), tmpDir, path, nil)
212228
if tt.expectErr != "" {
213229
g.Expect(err).ToNot(BeNil())
214230
g.Expect(err.Error()).To(ContainSubstring(tt.expectErr))
215231
g.Expect(cc).To(BeNil())
216232
return
217233
}
218234

235+
// Check successful checkout results.
236+
g.Expect(git.IsConcreteCommit(*cc)).To(Equal(tt.expectConcreteCommit))
237+
targetTagHash := tagCommits[tt.checkoutTag]
219238
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))
239+
g.Expect(cc.String()).To(Equal(tt.checkoutTag + "/" + targetTagHash))
240+
241+
// Check file content only when there's an actual checkout.
242+
if tt.lastRevTag != tt.checkoutTag {
243+
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
244+
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.checkoutTag))
245+
}
223246
})
224247
}
225248
}

pkg/git/libgit2/checkout.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,15 @@ func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *g
8181
return nil, fmt.Errorf("unable to remote ls for '%s': %w", managed.EffectiveURL(url), gitutil.LibGit2Error(err))
8282
}
8383
if len(heads) > 0 {
84-
currentRevision := fmt.Sprintf("%s/%s", c.Branch, heads[0].Id.String())
84+
hash := heads[0].Id.String()
85+
currentRevision := fmt.Sprintf("%s/%s", c.Branch, hash)
8586
if currentRevision == c.LastRevision {
86-
return nil, git.NoChangesError{
87-
Message: "no changes since last reconciliation",
88-
ObservedRevision: currentRevision,
87+
// Construct a partial commit with the existing information.
88+
c := &git.Commit{
89+
Hash: git.Hash(hash),
90+
Reference: "refs/heads/" + c.Branch,
8991
}
92+
return c, nil
9093
}
9194
}
9295
}
@@ -163,21 +166,25 @@ func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git.
163166
return nil, fmt.Errorf("unable to remote ls for '%s': %w", managed.EffectiveURL(url), gitutil.LibGit2Error(err))
164167
}
165168
if len(heads) > 0 {
166-
currentRevision := fmt.Sprintf("%s/%s", c.Tag, heads[0].Id.String())
169+
hash := heads[0].Id.String()
170+
currentRevision := fmt.Sprintf("%s/%s", c.Tag, hash)
167171
var same bool
168172
if currentRevision == c.LastRevision {
169173
same = true
170174
} else if len(heads) > 1 {
171-
currentAnnotatedRevision := fmt.Sprintf("%s/%s", c.Tag, heads[1].Id.String())
175+
hash = heads[1].Id.String()
176+
currentAnnotatedRevision := fmt.Sprintf("%s/%s", c.Tag, hash)
172177
if currentAnnotatedRevision == c.LastRevision {
173178
same = true
174179
}
175180
}
176181
if same {
177-
return nil, git.NoChangesError{
178-
Message: "no changes since last reconciliation",
179-
ObservedRevision: currentRevision,
182+
// Construct a partial commit with the existing information.
183+
c := &git.Commit{
184+
Hash: git.Hash(hash),
185+
Reference: "refs/tags/" + c.Tag,
180186
}
187+
return c, nil
181188
}
182189
}
183190
}

0 commit comments

Comments
 (0)