From b7376ce94c5886a5c072ae8fbb271756137eb8aa Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Sun, 24 Oct 2021 11:16:52 +0200 Subject: [PATCH] gogit: allow checkout of commit without branch This commit changes the `gogit` behavior for commit checkouts, now allowing one to reference to just a commit while omitting any branch reference. Doing this creates an Artifact with a `HEAD/` revision. If both a `branch` and `commit` are defined, the commit is expected to exist within the branch. This results in a more efficient clone of just the target branch, and also makes this change backwards compatible. Fixes #407 Fixes #315 Signed-off-by: Hidde Beydals --- api/v1beta1/gitrepository_types.go | 1 - ...rce.toolkit.fluxcd.io_gitrepositories.yaml | 1 - controllers/gitrepository_controller.go | 17 +-- controllers/gitrepository_controller_test.go | 2 +- pkg/git/git.go | 2 + pkg/git/gogit/checkout.go | 103 +++++++++--------- pkg/git/gogit/checkout_test.go | 95 +++++++++++----- pkg/git/gogit/gogit.go | 23 ++++ pkg/git/libgit2/checkout.go | 62 +++++------ pkg/git/libgit2/checkout_test.go | 11 +- pkg/git/libgit2/libgit2.go | 23 ++++ pkg/git/options.go | 17 ++- pkg/git/strategy/strategy.go | 18 +-- 13 files changed, 239 insertions(+), 136 deletions(-) create mode 100644 pkg/git/gogit/gogit.go create mode 100644 pkg/git/libgit2/libgit2.go diff --git a/api/v1beta1/gitrepository_types.go b/api/v1beta1/gitrepository_types.go index 6c178d02..b2471df0 100644 --- a/api/v1beta1/gitrepository_types.go +++ b/api/v1beta1/gitrepository_types.go @@ -120,7 +120,6 @@ type GitRepositoryInclude struct { // GitRepositoryRef defines the Git ref used for pull and checkout operations. type GitRepositoryRef struct { // The Git branch to checkout, defaults to master. - // +kubebuilder:default:=master // +optional Branch string `json:"branch,omitempty"` diff --git a/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml b/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml index dffd8599..f6f523ed 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml @@ -91,7 +91,6 @@ spec: description: The Git reference to checkout and monitor for changes, defaults to master branch. properties: branch: - default: master description: The Git branch to checkout, defaults to master. type: string commit: diff --git a/controllers/gitrepository_controller.go b/controllers/gitrepository_controller.go index b0939974..a8c75a03 100644 --- a/controllers/gitrepository_controller.go +++ b/controllers/gitrepository_controller.go @@ -249,14 +249,15 @@ func (r *GitRepositoryReconciler) reconcile(ctx context.Context, repository sour return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err } } - - checkoutStrategy, err := strategy.CheckoutStrategyForRef( - repository.Spec.Reference, - git.CheckoutOptions{ - GitImplementation: repository.Spec.GitImplementation, - RecurseSubmodules: repository.Spec.RecurseSubmodules, - }, - ) + checkoutOpts := git.CheckoutOptions{RecurseSubmodules: repository.Spec.RecurseSubmodules} + if ref := repository.Spec.Reference; ref != nil { + checkoutOpts.Branch = ref.Branch + checkoutOpts.Commit = ref.Commit + checkoutOpts.Tag = ref.Tag + checkoutOpts.SemVer = ref.SemVer + } + checkoutStrategy, err := strategy.CheckoutStrategyForImplementation(ctx, + git.Implementation(repository.Spec.GitImplementation), checkoutOpts) if err != nil { return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err } diff --git a/controllers/gitrepository_controller_test.go b/controllers/gitrepository_controller_test.go index 0ff13da5..46232547 100644 --- a/controllers/gitrepository_controller_test.go +++ b/controllers/gitrepository_controller_test.go @@ -263,7 +263,7 @@ var _ = Describe("GitRepositoryReconciler", func() { }, waitForReason: sourcev1.GitOperationSucceedReason, expectStatus: metav1.ConditionTrue, - expectRevision: "master", + expectRevision: "HEAD", }), Entry("commit in branch", refTestCase{ reference: &sourcev1.GitRepositoryRef{ diff --git a/pkg/git/git.go b/pkg/git/git.go index 5fae158b..59744ead 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -26,6 +26,8 @@ import ( "github.com/ProtonMail/go-crypto/openpgp" ) +type Implementation string + type Hash []byte // String returns the SHA1 Hash as a string. diff --git a/pkg/git/gogit/checkout.go b/pkg/git/gogit/checkout.go index 96818cac..14f1ecfb 100644 --- a/pkg/git/gogit/checkout.go +++ b/pkg/git/gogit/checkout.go @@ -25,40 +25,38 @@ import ( "time" "github.com/Masterminds/semver/v3" - "github.com/fluxcd/pkg/gitutil" - "github.com/fluxcd/pkg/version" extgogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + "github.com/fluxcd/pkg/gitutil" + "github.com/fluxcd/pkg/version" + "github.com/fluxcd/source-controller/pkg/git" ) -func CheckoutStrategyForRef(ref *sourcev1.GitRepositoryRef, opt git.CheckoutOptions) git.CheckoutStrategy { +// CheckoutStrategyForOptions returns the git.CheckoutStrategy for the given +// git.CheckoutOptions. +func CheckoutStrategyForOptions(_ context.Context, opts git.CheckoutOptions) git.CheckoutStrategy { switch { - case ref == nil: - return &CheckoutBranch{branch: git.DefaultBranch} - case ref.SemVer != "": - return &CheckoutSemVer{semVer: ref.SemVer, recurseSubmodules: opt.RecurseSubmodules} - case ref.Tag != "": - return &CheckoutTag{tag: ref.Tag, recurseSubmodules: opt.RecurseSubmodules} - case ref.Commit != "": - strategy := &CheckoutCommit{branch: ref.Branch, commit: ref.Commit, recurseSubmodules: opt.RecurseSubmodules} - if strategy.branch == "" { - strategy.branch = git.DefaultBranch - } - return strategy - case ref.Branch != "": - return &CheckoutBranch{branch: ref.Branch, recurseSubmodules: opt.RecurseSubmodules} + case opts.Commit != "": + return &CheckoutCommit{Branch: opts.Branch, Commit: opts.Commit, RecurseSubmodules: opts.RecurseSubmodules} + case opts.SemVer != "": + return &CheckoutSemVer{SemVer: opts.SemVer, RecurseSubmodules: opts.RecurseSubmodules} + case opts.Tag != "": + return &CheckoutTag{Tag: opts.Tag, RecurseSubmodules: opts.RecurseSubmodules} default: - return &CheckoutBranch{branch: git.DefaultBranch} + branch := opts.Branch + if branch == "" { + branch = git.DefaultBranch + } + return &CheckoutBranch{Branch: branch, RecurseSubmodules: opts.RecurseSubmodules} } } type CheckoutBranch struct { - branch string - recurseSubmodules bool + Branch string + RecurseSubmodules bool } func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) { @@ -66,16 +64,16 @@ func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *g if err != nil { return nil, fmt.Errorf("failed to construct auth method with options: %w", err) } - ref := plumbing.NewBranchReferenceName(c.branch) + ref := plumbing.NewBranchReferenceName(c.Branch) repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{ URL: url, Auth: authMethod, RemoteName: git.DefaultOrigin, - ReferenceName: plumbing.NewBranchReferenceName(c.branch), + ReferenceName: plumbing.NewBranchReferenceName(c.Branch), SingleBranch: true, NoCheckout: false, Depth: 1, - RecurseSubmodules: recurseSubmodules(c.recurseSubmodules), + RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules), Progress: nil, Tags: extgogit.NoTags, CABundle: caBundle(opts), @@ -85,7 +83,7 @@ func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *g } head, err := repo.Head() if err != nil { - return nil, fmt.Errorf("failed to resolve HEAD of branch '%s': %w", c.branch, err) + return nil, fmt.Errorf("failed to resolve HEAD of branch '%s': %w", c.Branch, err) } cc, err := repo.CommitObject(head.Hash()) if err != nil { @@ -95,8 +93,8 @@ func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *g } type CheckoutTag struct { - tag string - recurseSubmodules bool + Tag string + RecurseSubmodules bool } func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) { @@ -104,16 +102,16 @@ func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git. if err != nil { return nil, fmt.Errorf("failed to construct auth method with options: %w", err) } - ref := plumbing.NewTagReferenceName(c.tag) + ref := plumbing.NewTagReferenceName(c.Tag) repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{ URL: url, Auth: authMethod, RemoteName: git.DefaultOrigin, - ReferenceName: plumbing.NewTagReferenceName(c.tag), + ReferenceName: plumbing.NewTagReferenceName(c.Tag), SingleBranch: true, NoCheckout: false, Depth: 1, - RecurseSubmodules: recurseSubmodules(c.recurseSubmodules), + RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules), Progress: nil, Tags: extgogit.NoTags, CABundle: caBundle(opts), @@ -123,7 +121,7 @@ func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git. } head, err := repo.Head() if err != nil { - return nil, fmt.Errorf("failed to resolve HEAD of tag '%s': %w", c.tag, err) + return nil, fmt.Errorf("failed to resolve HEAD of tag '%s': %w", c.Tag, err) } cc, err := repo.CommitObject(head.Hash()) if err != nil { @@ -133,9 +131,9 @@ func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git. } type CheckoutCommit struct { - branch string - commit string - recurseSubmodules bool + Branch string + Commit string + RecurseSubmodules bool } func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) { @@ -143,19 +141,22 @@ func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, opts *g if err != nil { return nil, fmt.Errorf("failed to construct auth method with options: %w", err) } - ref := plumbing.NewBranchReferenceName(c.branch) - repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{ + cloneOpts := &extgogit.CloneOptions{ URL: url, Auth: authMethod, RemoteName: git.DefaultOrigin, - ReferenceName: ref, - SingleBranch: true, - NoCheckout: false, - RecurseSubmodules: recurseSubmodules(c.recurseSubmodules), + SingleBranch: false, + NoCheckout: true, + RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules), Progress: nil, Tags: extgogit.NoTags, CABundle: caBundle(opts), - }) + } + if c.Branch != "" { + cloneOpts.SingleBranch = true + cloneOpts.ReferenceName = plumbing.NewBranchReferenceName(c.Branch) + } + repo, err := extgogit.PlainCloneContext(ctx, path, false, cloneOpts) if err != nil { return nil, fmt.Errorf("unable to clone '%s', error: %w", url, gitutil.GoGitError(err)) } @@ -163,29 +164,27 @@ func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, opts *g if err != nil { return nil, fmt.Errorf("failed to open Git worktree: %w", err) } - f, _ := repo.Head() - f.String() - cc, err := repo.CommitObject(plumbing.NewHash(c.commit)) + cc, err := repo.CommitObject(plumbing.NewHash(c.Commit)) if err != nil { - return nil, fmt.Errorf("failed to resolve commit object for '%s': %w", c.commit, err) + return nil, fmt.Errorf("failed to resolve commit object for '%s': %w", c.Commit, err) } err = w.Checkout(&extgogit.CheckoutOptions{ Hash: cc.Hash, Force: true, }) if err != nil { - return nil, fmt.Errorf("failed to checkout commit '%s': %w", c.commit, err) + return nil, fmt.Errorf("failed to checkout commit '%s': %w", c.Commit, err) } - return commitWithRef(cc, ref) + return commitWithRef(cc, cloneOpts.ReferenceName) } type CheckoutSemVer struct { - semVer string - recurseSubmodules bool + SemVer string + RecurseSubmodules bool } func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) { - verConstraint, err := semver.NewConstraint(c.semVer) + verConstraint, err := semver.NewConstraint(c.SemVer) if err != nil { return nil, fmt.Errorf("semver parse error: %w", err) } @@ -201,7 +200,7 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, opts *g RemoteName: git.DefaultOrigin, NoCheckout: false, Depth: 1, - RecurseSubmodules: recurseSubmodules(c.recurseSubmodules), + RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules), Progress: nil, Tags: extgogit.AllTags, CABundle: caBundle(opts), @@ -247,7 +246,7 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, opts *g matchedVersions = append(matchedVersions, v) } if len(matchedVersions) == 0 { - return nil, fmt.Errorf("no match found for semver: %s", c.semVer) + return nil, fmt.Errorf("no match found for semver: %s", c.SemVer) } // Sort versions diff --git a/pkg/git/gogit/checkout_test.go b/pkg/git/gogit/checkout_test.go index c82e0d3b..37367852 100644 --- a/pkg/git/gogit/checkout_test.go +++ b/pkg/git/gogit/checkout_test.go @@ -83,7 +83,7 @@ func TestCheckoutBranch_Checkout(t *testing.T) { g := NewWithT(t) branch := CheckoutBranch{ - branch: tt.branch, + Branch: tt.branch, } tmpDir, _ := os.MkdirTemp("", "test") defer os.RemoveAll(tmpDir) @@ -152,7 +152,7 @@ func TestCheckoutTag_Checkout(t *testing.T) { } tag := CheckoutTag{ - tag: tt.checkoutTag, + Tag: tt.checkoutTag, } tmpDir, _ := os.MkdirTemp("", "test") defer os.RemoveAll(tmpDir) @@ -173,46 +173,87 @@ func TestCheckoutTag_Checkout(t *testing.T) { } 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()) + firstCommit, err := commitFile(repo, "commit", "init", time.Now()) if err != nil { t.Fatal(err) } - if _, err = commitFile(repo, "commit", "second", time.Now()); err != nil { + if err = createBranch(repo, "other-branch"); err != nil { + t.Fatal(err) + } + secondCommit, err := commitFile(repo, "commit", "second", time.Now()) + if err != nil { t.Fatal(err) } - commit := CheckoutCommit{ - commit: c.String(), - branch: "master", + tests := []struct { + name string + commit string + branch string + expectCommit string + expectFile string + expectError string + }{ + { + name: "Commit", + commit: firstCommit.String(), + expectCommit: "HEAD/" + firstCommit.String(), + expectFile: "init", + }, + { + name: "Commit in specific branch", + commit: secondCommit.String(), + branch: "other-branch", + expectCommit: "other-branch/" + secondCommit.String(), + expectFile: "second", + }, + { + name: "Non existing commit", + commit: "a-random-invalid-commit", + expectError: "failed to resolve commit object for 'a-random-invalid-commit': object not found", + }, + { + name: "Non existing commit in specific branch", + commit: secondCommit.String(), + branch: "master", + expectError: "object not found", + }, } - tmpDir, _ := os.MkdirTemp("", "git2go") - defer os.RemoveAll(tmpDir) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) - cc, err := commit.Checkout(context.TODO(), tmpDir, path, nil) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cc.String()).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: tt.commit, + Branch: tt.branch, + } - commit = CheckoutCommit{ - commit: "4dc3185c5fc94eb75048376edeb44571cece25f4", - branch: "master", + tmpDir, err := os.MkdirTemp("", "git2go") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + cc, err := commit.Checkout(context.TODO(), tmpDir, path, nil) + if tt.expectError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.expectError)) + g.Expect(cc).To(BeNil()) + return + } + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cc).ToNot(BeNil()) + g.Expect(cc.String()).To(Equal(tt.expectCommit)) + g.Expect(filepath.Join(tmpDir, "commit")).To(BeARegularFile()) + g.Expect(os.ReadFile(filepath.Join(tmpDir, "commit"))).To(BeEquivalentTo(tt.expectFile)) + }) } - tmpDir2, _ := os.MkdirTemp("", "git2go") - defer os.RemoveAll(tmpDir) - - cc, err = commit.Checkout(context.TODO(), tmpDir2, path, nil) - g.Expect(err).To(HaveOccurred()) - g.Expect(err.Error()).To(ContainSubstring("object not found")) - g.Expect(cc).To(BeNil()) } func TestCheckoutTagSemVer_Checkout(t *testing.T) { @@ -300,7 +341,7 @@ func TestCheckoutTagSemVer_Checkout(t *testing.T) { g := NewWithT(t) semVer := CheckoutSemVer{ - semVer: tt.constraint, + SemVer: tt.constraint, } tmpDir, _ := os.MkdirTemp("", "test") defer os.RemoveAll(tmpDir) diff --git a/pkg/git/gogit/gogit.go b/pkg/git/gogit/gogit.go new file mode 100644 index 00000000..2ce0a864 --- /dev/null +++ b/pkg/git/gogit/gogit.go @@ -0,0 +1,23 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gogit + +import "github.com/fluxcd/source-controller/pkg/git" + +const ( + Implementation git.Implementation = "go-git" +) diff --git a/pkg/git/libgit2/checkout.go b/pkg/git/libgit2/checkout.go index 0dfbbcd2..e0c4d1ed 100644 --- a/pkg/git/libgit2/checkout.go +++ b/pkg/git/libgit2/checkout.go @@ -24,38 +24,39 @@ import ( "time" "github.com/Masterminds/semver/v3" + "github.com/go-logr/logr" git2go "github.com/libgit2/git2go/v31" "github.com/fluxcd/pkg/gitutil" "github.com/fluxcd/pkg/version" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" "github.com/fluxcd/source-controller/pkg/git" ) -func CheckoutStrategyForRef(ref *sourcev1.GitRepositoryRef, opt git.CheckoutOptions) git.CheckoutStrategy { +// CheckoutStrategyForOptions returns the git.CheckoutStrategy for the given +// git.CheckoutOptions. +func CheckoutStrategyForOptions(ctx context.Context, opt git.CheckoutOptions) git.CheckoutStrategy { + if opt.RecurseSubmodules { + logr.FromContextOrDiscard(ctx).Info("git submodule recursion not supported by '%s'", Implementation) + } switch { - case ref == nil: - return &CheckoutBranch{branch: git.DefaultBranch} - case ref.SemVer != "": - return &CheckoutSemVer{semVer: ref.SemVer} - case ref.Tag != "": - return &CheckoutTag{tag: ref.Tag} - case ref.Commit != "": - strategy := &CheckoutCommit{branch: ref.Branch, commit: ref.Commit} - if strategy.branch == "" { - strategy.branch = git.DefaultBranch - } - return strategy - case ref.Branch != "": - return &CheckoutBranch{branch: ref.Branch} + case opt.Commit != "": + return &CheckoutCommit{Commit: opt.Commit} + case opt.SemVer != "": + return &CheckoutSemVer{SemVer: opt.SemVer} + case opt.Tag != "": + return &CheckoutTag{Tag: opt.Tag} default: - return &CheckoutBranch{branch: git.DefaultBranch} + branch := opt.Branch + if branch == "" { + branch = git.DefaultBranch + } + return &CheckoutBranch{Branch: branch} } } type CheckoutBranch struct { - branch string + Branch string } func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) { @@ -64,7 +65,7 @@ func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *g DownloadTags: git2go.DownloadTagsNone, RemoteCallbacks: remoteCallbacks(opts), }, - CheckoutBranch: c.branch, + CheckoutBranch: c.Branch, }) if err != nil { return nil, fmt.Errorf("unable to clone '%s', error: %w", url, gitutil.LibGit2Error(err)) @@ -77,14 +78,14 @@ func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *g defer head.Free() cc, err := repo.LookupCommit(head.Target()) if err != nil { - return nil, fmt.Errorf("could not find commit '%s' in branch '%s': %w", head.Target(), c.branch, err) + return nil, fmt.Errorf("could not find commit '%s' in branch '%s': %w", head.Target(), c.Branch, err) } defer cc.Free() - return commit(cc, "refs/heads/"+c.branch), nil + return commit(cc, "refs/heads/"+c.Branch), nil } type CheckoutTag struct { - tag string + Tag string } func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) { @@ -98,17 +99,16 @@ func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git. return nil, fmt.Errorf("unable to clone '%s', error: %w", url, gitutil.LibGit2Error(err)) } defer repo.Free() - cc, err := checkoutDetachedDwim(repo, c.tag) + cc, err := checkoutDetachedDwim(repo, c.Tag) if err != nil { return nil, err } defer cc.Free() - return commit(cc, "refs/tags/"+c.tag), nil + return commit(cc, "refs/tags/"+c.Tag), nil } type CheckoutCommit struct { - branch string - commit string + Commit string } func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) { @@ -122,9 +122,9 @@ func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, opts *g return nil, fmt.Errorf("unable to clone '%s', error: %w", url, gitutil.LibGit2Error(err)) } defer repo.Free() - oid, err := git2go.NewOid(c.commit) + oid, err := git2go.NewOid(c.Commit) if err != nil { - return nil, fmt.Errorf("could not create oid for '%s': %w", c.commit, err) + return nil, fmt.Errorf("could not create oid for '%s': %w", c.Commit, err) } cc, err := checkoutDetachedHEAD(repo, oid) if err != nil { @@ -134,11 +134,11 @@ func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, opts *g } type CheckoutSemVer struct { - semVer string + SemVer string } func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) { - verConstraint, err := semver.NewConstraint(c.semVer) + verConstraint, err := semver.NewConstraint(c.SemVer) if err != nil { return nil, fmt.Errorf("semver parse error: %w", err) } @@ -202,7 +202,7 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, opts *g matchedVersions = append(matchedVersions, v) } if len(matchedVersions) == 0 { - return nil, fmt.Errorf("no match found for semver: %s", c.semVer) + return nil, fmt.Errorf("no match found for semver: %s", c.SemVer) } // Sort versions diff --git a/pkg/git/libgit2/checkout_test.go b/pkg/git/libgit2/checkout_test.go index 8a077a92..24ca72b3 100644 --- a/pkg/git/libgit2/checkout_test.go +++ b/pkg/git/libgit2/checkout_test.go @@ -77,7 +77,7 @@ func TestCheckoutBranch_Checkout(t *testing.T) { g := NewWithT(t) branch := CheckoutBranch{ - branch: tt.branch, + Branch: tt.branch, } tmpDir, _ := os.MkdirTemp("", "test") defer os.RemoveAll(tmpDir) @@ -148,7 +148,7 @@ func TestCheckoutTag_Checkout(t *testing.T) { } tag := CheckoutTag{ - tag: tt.checkoutTag, + Tag: tt.checkoutTag, } tmpDir, _ := os.MkdirTemp("", "test") defer os.RemoveAll(tmpDir) @@ -188,8 +188,7 @@ func TestCheckoutCommit_Checkout(t *testing.T) { } commit := CheckoutCommit{ - commit: c.String(), - branch: "main", + Commit: c.String(), } tmpDir, _ := os.MkdirTemp("", "git2go") defer os.RemoveAll(tmpDir) @@ -202,7 +201,7 @@ func TestCheckoutCommit_Checkout(t *testing.T) { g.Expect(os.ReadFile(filepath.Join(tmpDir, "commit"))).To(BeEquivalentTo("init")) commit = CheckoutCommit{ - commit: "4dc3185c5fc94eb75048376edeb44571cece25f4", + Commit: "4dc3185c5fc94eb75048376edeb44571cece25f4", } tmpDir2, _ := os.MkdirTemp("", "git2go") defer os.RemoveAll(tmpDir) @@ -309,7 +308,7 @@ func TestCheckoutTagSemVer_Checkout(t *testing.T) { g := NewWithT(t) semVer := CheckoutSemVer{ - semVer: tt.constraint, + SemVer: tt.constraint, } tmpDir, _ := os.MkdirTemp("", "test") defer os.RemoveAll(tmpDir) diff --git a/pkg/git/libgit2/libgit2.go b/pkg/git/libgit2/libgit2.go new file mode 100644 index 00000000..e705e6b0 --- /dev/null +++ b/pkg/git/libgit2/libgit2.go @@ -0,0 +1,23 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package libgit2 + +import "github.com/fluxcd/source-controller/pkg/git" + +const ( + Implementation git.Implementation = "libgit2" +) diff --git a/pkg/git/options.go b/pkg/git/options.go index 6dbfe6ab..bacfd737 100644 --- a/pkg/git/options.go +++ b/pkg/git/options.go @@ -32,7 +32,22 @@ const ( // CheckoutOptions are the options used for a Git checkout. type CheckoutOptions struct { - GitImplementation string + // Branch to checkout, can be combined with Branch with some + // Implementations. + Branch string + + // Tag to checkout, takes precedence over Branch. + Tag string + + // SemVer tag expression to checkout, takes precedence over Tag. + SemVer string `json:"semver,omitempty"` + + // Commit SHA1 to checkout, takes precedence over Tag and SemVer, + // can be combined with Branch with some Implementations. + Commit string + + // RecurseSubmodules defines if submodules should be checked out, + // not supported by all Implementations. RecurseSubmodules bool } diff --git a/pkg/git/strategy/strategy.go b/pkg/git/strategy/strategy.go index b6924f89..46d4e58a 100644 --- a/pkg/git/strategy/strategy.go +++ b/pkg/git/strategy/strategy.go @@ -17,21 +17,23 @@ limitations under the License. package strategy import ( + "context" "fmt" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" "github.com/fluxcd/source-controller/pkg/git" "github.com/fluxcd/source-controller/pkg/git/gogit" "github.com/fluxcd/source-controller/pkg/git/libgit2" ) -func CheckoutStrategyForRef(ref *sourcev1.GitRepositoryRef, opt git.CheckoutOptions) (git.CheckoutStrategy, error) { - switch opt.GitImplementation { - case sourcev1.GoGitImplementation: - return gogit.CheckoutStrategyForRef(ref, opt), nil - case sourcev1.LibGit2Implementation: - return libgit2.CheckoutStrategyForRef(ref, opt), nil +// CheckoutStrategyForImplementation returns the CheckoutStrategy for the given +// git.Implementation and git.CheckoutOptions. +func CheckoutStrategyForImplementation(ctx context.Context, impl git.Implementation, opts git.CheckoutOptions) (git.CheckoutStrategy, error) { + switch impl { + case gogit.Implementation: + return gogit.CheckoutStrategyForOptions(ctx, opts), nil + case libgit2.Implementation: + return libgit2.CheckoutStrategyForOptions(ctx, opts), nil default: - return nil, fmt.Errorf("unsupported Git implementation %s", opt.GitImplementation) + return nil, fmt.Errorf("unsupported Git implementation '%s'", impl) } }