diff --git a/pkg/git/gogit/checkout.go b/pkg/git/gogit/checkout.go index 0200669c..5744654e 100644 --- a/pkg/git/gogit/checkout.go +++ b/pkg/git/gogit/checkout.go @@ -74,7 +74,7 @@ func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *g RecurseSubmodules: recurseSubmodules(c.recurseSubmodules), Progress: nil, Tags: extgogit.NoTags, - CABundle: opts.CAFile, + CABundle: caBundle(opts), }) if err != nil { return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, gitutil.GoGitError(err)) @@ -111,7 +111,7 @@ func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git. RecurseSubmodules: recurseSubmodules(c.recurseSubmodules), Progress: nil, Tags: extgogit.NoTags, - CABundle: opts.CAFile, + CABundle: caBundle(opts), }) if err != nil { return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, err) @@ -148,7 +148,7 @@ func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, opts *g RecurseSubmodules: recurseSubmodules(c.recurseSubmodules), Progress: nil, Tags: extgogit.NoTags, - CABundle: opts.CAFile, + CABundle: caBundle(opts), }) if err != nil { return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, err) @@ -194,7 +194,7 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, opts *g RecurseSubmodules: recurseSubmodules(c.recurseSubmodules), Progress: nil, Tags: extgogit.AllTags, - CABundle: opts.CAFile, + CABundle: caBundle(opts), }) if err != nil { return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, err) diff --git a/pkg/git/gogit/checkout_test.go b/pkg/git/gogit/checkout_test.go index 3dbf59a3..ce7a5e7e 100644 --- a/pkg/git/gogit/checkout_test.go +++ b/pkg/git/gogit/checkout_test.go @@ -18,37 +18,379 @@ package gogit import ( "context" + "errors" "os" + "path/filepath" "testing" + "time" - "github.com/fluxcd/source-controller/pkg/git" + "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-billy/v5/osfs" + extgogit "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/cache" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/storage/filesystem" + . "github.com/onsi/gomega" ) -func TestCheckoutTagSemVer_Checkout(t *testing.T) { - auth := &git.AuthOptions{} - tag := CheckoutTag{ - tag: "v1.7.0", - } - tmpDir, _ := os.MkdirTemp("", "test") - defer os.RemoveAll(tmpDir) - - cTag, _, err := tag.Checkout(context.TODO(), tmpDir, "https://github.com/projectcontour/contour", auth) +func TestCheckoutBranch_Checkout(t *testing.T) { + repo, path, err := initRepo() if err != nil { - t.Error(err) + t.Fatal(err) } + defer os.RemoveAll(path) - semVer := CheckoutSemVer{ - semVer: ">=1.0.0 <=1.7.0", - } - tmpDir2, _ := os.MkdirTemp("", "test") - defer os.RemoveAll(tmpDir2) - - cSemVer, _, err := semVer.Checkout(context.TODO(), tmpDir2, "https://github.com/projectcontour/contour", auth) + firstCommit, err := commitFile(repo, "branch", "init", time.Now()) if err != nil { - t.Error(err) + t.Fatal(err) } - if cTag.Hash() != cSemVer.Hash() { - t.Errorf("expected semver hash %s, got %s", cTag.Hash(), cSemVer.Hash()) + if err = createBranch(repo, "test"); err != nil { + t.Fatal(err) + } + + secondCommit, err := commitFile(repo, "branch", "second", time.Now()) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + branch string + expectedCommit string + expectedErr string + }{ + { + name: "Default branch", + branch: "master", + expectedCommit: firstCommit.String(), + }, + { + name: "Other branch", + branch: "test", + expectedCommit: secondCommit.String(), + }, + { + name: "Non existing branch", + branch: "invalid", + expectedErr: "couldn't find remote ref \"refs/heads/invalid\"", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + branch := CheckoutBranch{ + branch: tt.branch, + } + tmpDir, _ := os.MkdirTemp("", "test") + defer os.RemoveAll(tmpDir) + + _, ref, err := branch.Checkout(context.TODO(), tmpDir, path, nil) + if tt.expectedErr != "" { + g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr)) + g.Expect(ref).To(BeEmpty()) + return + } + g.Expect(err).To(BeNil()) + g.Expect(ref).To(Equal(tt.branch + "/" + tt.expectedCommit)) + }) + } +} + +func TestCheckoutTag_Checkout(t *testing.T) { + tests := []struct { + name string + tag string + annotated bool + checkoutTag string + expectTag string + expectErr string + }{ + { + name: "Tag", + tag: "tag-1", + checkoutTag: "tag-1", + expectTag: "tag-1", + }, + { + name: "Annotated", + tag: "annotated", + annotated: true, + checkoutTag: "annotated", + expectTag: "annotated", + }, + { + name: "Non existing tag", + tag: "tag-1", + checkoutTag: "invalid", + expectErr: "error: couldn't find remote ref \"refs/tags/invalid\"", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + repo, path, err := initRepo() + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(path) + + var h plumbing.Hash + if tt.tag != "" { + h, err = commitFile(repo, "tag", tt.tag, time.Now()) + if err != nil { + t.Fatal(err) + } + _, err = tag(repo, h, !tt.annotated, tt.tag, time.Now()) + if err != nil { + t.Fatal(err) + } + } + + tag := CheckoutTag{ + tag: tt.checkoutTag, + } + tmpDir, _ := os.MkdirTemp("", "test") + defer os.RemoveAll(tmpDir) + + _, ref, err := tag.Checkout(context.TODO(), tmpDir, path, nil) + if tt.expectErr != "" { + g.Expect(err.Error()).To(ContainSubstring(tt.expectErr)) + g.Expect(ref).To(BeEmpty()) + return + } + if tt.expectTag != "" { + g.Expect(ref).To(Equal(tt.expectTag + "/" + h.String())) + g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile()) + g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.tag)) + } + }) + } +} + +func TestCheckoutCommit_Checkout(t *testing.T) { + g := NewWithT(t) + + repo, path, err := initRepo() + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(path) + + c, err := commitFile(repo, "commit", "init", time.Now()) + if err != nil { + t.Fatal(err) + } + if _, err = commitFile(repo, "commit", "second", time.Now()); err != nil { + t.Fatal(err) + } + + commit := CheckoutCommit{ + commit: c.String(), + branch: "master", + } + tmpDir, _ := os.MkdirTemp("", "git2go") + defer os.RemoveAll(tmpDir) + + _, ref, err := commit.Checkout(context.TODO(), tmpDir, path, nil) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(ref).To(Equal("master/" + c.String())) + g.Expect(filepath.Join(tmpDir, "commit")).To(BeARegularFile()) + g.Expect(os.ReadFile(filepath.Join(tmpDir, "commit"))).To(BeEquivalentTo("init")) + + commit = CheckoutCommit{ + commit: "4dc3185c5fc94eb75048376edeb44571cece25f4", + branch: "master", + } + tmpDir2, _ := os.MkdirTemp("", "git2go") + defer os.RemoveAll(tmpDir) + + _, ref, err = commit.Checkout(context.TODO(), tmpDir2, path, nil) + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(HavePrefix("git commit '4dc3185c5fc94eb75048376edeb44571cece25f4' not found:")) + g.Expect(ref).To(BeEmpty()) +} + +func TestCheckoutTagSemVer_Checkout(t *testing.T) { + now := time.Now() + + tags := []struct { + tag string + annotated bool + commitTime time.Time + tagTime time.Time + }{ + { + tag: "v0.0.1", + annotated: false, + commitTime: now, + }, + { + tag: "v0.1.0+build-1", + annotated: true, + commitTime: now.Add(10 * time.Minute), + tagTime: now.Add(2 * time.Hour), // This should be ignored during TS comparisons + }, + { + tag: "v0.1.0+build-2", + annotated: false, + commitTime: now.Add(30 * time.Minute), + }, + { + tag: "v0.1.0+build-3", + annotated: true, + commitTime: now.Add(1 * time.Hour), + tagTime: now.Add(1 * time.Hour), // This should be ignored during TS comparisons + }, + { + tag: "0.2.0", + annotated: true, + commitTime: now, + tagTime: now, + }, + } + tests := []struct { + name string + constraint string + expectErr error + expectTag string + }{ + { + name: "Orders by SemVer", + constraint: ">0.1.0", + expectTag: "0.2.0", + }, + { + name: "Orders by SemVer and timestamp", + constraint: "<0.2.0", + expectTag: "v0.1.0+build-3", + }, + { + name: "Errors without match", + constraint: ">=1.0.0", + expectErr: errors.New("no match found for semver: >=1.0.0"), + }, + } + + repo, path, err := initRepo() + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(path) + + refs := make(map[string]string, len(tags)) + for _, tt := range tags { + ref, err := commitFile(repo, "tag", tt.tag, tt.commitTime) + if err != nil { + t.Fatal(err) + } + _, err = tag(repo, ref, tt.annotated, tt.tag, tt.tagTime) + if err != nil { + t.Fatal(err) + } + refs[tt.tag] = ref.String() + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + semVer := CheckoutSemVer{ + semVer: tt.constraint, + } + tmpDir, _ := os.MkdirTemp("", "test") + defer os.RemoveAll(tmpDir) + + _, ref, err := semVer.Checkout(context.TODO(), tmpDir, path, nil) + if tt.expectErr != nil { + g.Expect(err).To(Equal(tt.expectErr)) + g.Expect(ref).To(BeEmpty()) + return + } + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(ref).To(Equal(tt.expectTag + "/" + refs[tt.expectTag])) + g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile()) + g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.expectTag)) + }) + } +} + +func initRepo() (*extgogit.Repository, string, error) { + tmpDir, err := os.MkdirTemp("", "gogit") + if err != nil { + os.RemoveAll(tmpDir) + return nil, "", err + } + sto := filesystem.NewStorage(osfs.New(tmpDir), cache.NewObjectLRUDefault()) + repo, err := extgogit.Init(sto, memfs.New()) + if err != nil { + os.RemoveAll(tmpDir) + return nil, "", err + } + return repo, tmpDir, err +} + +func createBranch(repo *extgogit.Repository, branch string) error { + wt, err := repo.Worktree() + if err != nil { + return err + } + h, err := repo.Head() + if err != nil { + return err + } + return wt.Checkout(&extgogit.CheckoutOptions{ + Hash: h.Hash(), + Branch: plumbing.ReferenceName("refs/heads/" + branch), + Create: true, + }) +} + +func commitFile(repo *extgogit.Repository, path, content string, time time.Time) (plumbing.Hash, error) { + wt, err := repo.Worktree() + if err != nil { + return plumbing.Hash{}, err + } + f, err := wt.Filesystem.Create(path) + if err != nil { + return plumbing.Hash{}, err + } + if _, err = f.Write([]byte(content)); err != nil { + f.Close() + return plumbing.Hash{}, err + } + if err = f.Close(); err != nil { + return plumbing.Hash{}, err + } + if _, err = wt.Add(path); err != nil { + return plumbing.Hash{}, err + } + return wt.Commit("Adding: "+path, &extgogit.CommitOptions{ + Author: signature(time), + Committer: signature(time), + }) +} + +func tag(repo *extgogit.Repository, commit plumbing.Hash, annotated bool, tag string, time time.Time) (*plumbing.Reference, error) { + var opts *extgogit.CreateTagOptions + if annotated { + opts = &extgogit.CreateTagOptions{ + Tagger: signature(time), + Message: "Annotated tag for: " + tag, + } + } + return repo.CreateTag(tag, commit, opts) +} + +func signature(time time.Time) *object.Signature { + return &object.Signature{ + Name: "Jane Doe", + Email: "jane@example.com", + When: time, } } diff --git a/pkg/git/gogit/transport.go b/pkg/git/gogit/transport.go index 113433a8..d30a2abd 100644 --- a/pkg/git/gogit/transport.go +++ b/pkg/git/gogit/transport.go @@ -29,6 +29,9 @@ import ( // transportAuth constructs the transport.AuthMethod for the git.Transport of // the given git.AuthOptions. It returns the result, or an error. func transportAuth(opts *git.AuthOptions) (transport.AuthMethod, error) { + if opts == nil { + return nil, nil + } switch opts.Transport { case git.HTTPS, git.HTTP: return &http.BasicAuth{ @@ -53,3 +56,11 @@ func transportAuth(opts *git.AuthOptions) (transport.AuthMethod, error) { } return nil, nil } + +// caBundle returns the CA bundle from the given git.AuthOptions. +func caBundle(opts *git.AuthOptions) []byte { + if opts == nil { + return nil + } + return opts.CAFile +} diff --git a/pkg/git/gogit/transport_test.go b/pkg/git/gogit/transport_test.go index 32ce2a3f..f1863f0e 100644 --- a/pkg/git/gogit/transport_test.go +++ b/pkg/git/gogit/transport_test.go @@ -193,3 +193,10 @@ func Test_transportAuth(t *testing.T) { }) } } + +func Test_caBundle(t *testing.T) { + g := NewWithT(t) + + g.Expect(caBundle(&git.AuthOptions{CAFile: []byte("foo")})).To(BeEquivalentTo("foo")) + g.Expect(caBundle(nil)).To(BeNil()) +} diff --git a/pkg/git/libgit2/checkout_test.go b/pkg/git/libgit2/checkout_test.go index 8e33a7a2..8c1d31c5 100644 --- a/pkg/git/libgit2/checkout_test.go +++ b/pkg/git/libgit2/checkout_test.go @@ -84,6 +84,7 @@ func TestCheckoutBranch_Checkout(t *testing.T) { _, ref, err := branch.Checkout(context.TODO(), tmpDir, repo.Path(), nil) if tt.expectedErr != "" { + g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr)) g.Expect(ref).To(BeEmpty()) return @@ -154,7 +155,8 @@ func TestCheckoutTag_Checkout(t *testing.T) { _, ref, err := tag.Checkout(context.TODO(), tmpDir, repo.Path(), nil) if tt.expectErr != "" { - g.Expect(err.Error()).To(Equal(tt.expectErr)) + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.expectErr)) g.Expect(ref).To(BeEmpty()) return } @@ -193,7 +195,7 @@ func TestCheckoutCommit_Checkout(t *testing.T) { defer os.RemoveAll(tmpDir) _, ref, err := commit.Checkout(context.TODO(), tmpDir, repo.Path(), nil) - g.Expect(err).To(BeNil()) + g.Expect(err).ToNot(HaveOccurred()) g.Expect(ref).To(Equal("main/" + c.String())) g.Expect(filepath.Join(tmpDir, "commit")).To(BeARegularFile()) g.Expect(os.ReadFile(filepath.Join(tmpDir, "commit"))).To(BeEquivalentTo("init")) @@ -205,6 +207,7 @@ func TestCheckoutCommit_Checkout(t *testing.T) { defer os.RemoveAll(tmpDir) _, ref, err = commit.Checkout(context.TODO(), tmpDir2, repo.Path(), nil) + g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(HavePrefix("git checkout error: git commit '4dc3185c5fc94eb75048376edeb44571cece25f4' not found:")) g.Expect(ref).To(BeEmpty()) } @@ -316,6 +319,7 @@ func TestCheckoutTagSemVer_Checkout(t *testing.T) { g.Expect(ref).To(BeEmpty()) return } + g.Expect(err).ToNot(HaveOccurred()) g.Expect(ref).To(Equal(tt.expectTag + "/" + refs[tt.expectTag])) g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())