Skip to content

Commit

Permalink
add support for checking out git repo to a ref
Browse files Browse the repository at this point in the history
Add a new checkout strategy that enables checking out to a Git
reference: https://git-scm.com/book/en/v2/Git-Internals-Git-References.

Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
  • Loading branch information
aryan9600 committed Feb 14, 2023
1 parent db1f3af commit 2faf04a
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 2 deletions.
2 changes: 2 additions & 0 deletions git/gogit/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ func (g *Client) Clone(ctx context.Context, url string, cloneOpts repository.Clo
switch {
case checkoutStrat.Commit != "":
return g.cloneCommit(ctx, url, checkoutStrat.Commit, cloneOpts)
case checkoutStrat.RefName != "":
return g.cloneRefName(ctx, url, checkoutStrat.RefName, cloneOpts)
case checkoutStrat.Tag != "":
return g.cloneTag(ctx, url, checkoutStrat.Tag, cloneOpts)
case checkoutStrat.SemVer != "":
Expand Down
32 changes: 32 additions & 0 deletions git/gogit/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func (g *Client) cloneBranch(ctx context.Context, url, branch string, opts repos
return nil, err
}
if head != "" && head == lastObserved {
// Construct a non-concrete commit with the existing information.
c := &git.Commit{
Hash: git.ExtractHashFromRevision(head),
Reference: plumbing.NewBranchReferenceName(branch).String(),
Expand Down Expand Up @@ -134,6 +135,7 @@ func (g *Client) cloneTag(ctx context.Context, url, tag string, opts repository.
return nil, err
}
if head != "" && head == lastObserved {
// Construct a non-concrete commit with the existing information.
c := &git.Commit{
Hash: git.ExtractHashFromRevision(head),
Reference: ref.String(),
Expand Down Expand Up @@ -354,6 +356,36 @@ func (g *Client) cloneSemVer(ctx context.Context, url, semverTag string, opts re
return buildCommitWithRef(cc, ref)
}

func (g *Client) cloneRefName(ctx context.Context, url string, refName string, cloneOpts repository.CloneOptions) (*git.Commit, error) {
authMethod, err := transportAuth(g.authOpts, g.useDefaultKnownHosts)
if err != nil {
return nil, fmt.Errorf("unable to construct auth method with options: %w", err)
}
head, err := getRemoteHEAD(ctx, url, plumbing.ReferenceName(refName), g.authOpts, authMethod)
if err != nil {
return nil, err
}
if head == "" {
return nil, fmt.Errorf("unable to resolve ref: %s to a specific commit", refName)
}

hash := git.ExtractHashFromRevision(head)
// check if previous revision has changed before attempting to clone
if lastObserved := git.TransformRevision(cloneOpts.LastObservedCommit); lastObserved != "" {
if hash.Digest() != "" && hash.Digest() == lastObserved {
// Construct a non-concrete commit with the existing information.
// We exclude the reference here to ensure compatibility with the format
// of the Commit object returned by cloneCommit().
c := &git.Commit{
Hash: hash,
}
return c, nil
}
}

return g.cloneCommit(ctx, url, hash.String(), cloneOpts)
}

func recurseSubmodules(recurse bool) extgogit.SubmoduleRescursivity {
if recurse {
return extgogit.DefaultSubmoduleRecursionDepth
Expand Down
170 changes: 170 additions & 0 deletions git/gogit/clone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"time"

extgogit "github.com/fluxcd/go-git/v5"
"github.com/fluxcd/go-git/v5/config"
"github.com/fluxcd/go-git/v5/plumbing"
"github.com/fluxcd/go-git/v5/plumbing/cache"
"github.com/fluxcd/go-git/v5/plumbing/object"
Expand Down Expand Up @@ -493,6 +494,175 @@ func TestClone_cloneSemVer(t *testing.T) {
}
}

func TestClone_cloneRefName(t *testing.T) {
server, err := gittestserver.NewTempGitServer()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(server.Root())
err = server.StartHTTP()
if err != nil {
t.Fatal(err)
}
defer server.StopHTTP()

repoPath := "test.git"
err = server.InitRepo("../testdata/git/repo", git.DefaultBranch, repoPath)
if err != nil {
t.Fatal(err)
}
repoURL := server.HTTPAddress() + "/" + repoPath
repo, err := extgogit.PlainClone(t.TempDir(), false, &extgogit.CloneOptions{
URL: repoURL,
})
if err != nil {
t.Fatal(err)
}

// head is the current HEAD on master
head, err := repo.Head()
if err != nil {
t.Fatal(err)
}
err = createBranch(repo, "test")
if err != nil {
t.Fatal(err)
}
err = repo.Push(&extgogit.PushOptions{})
if err != nil {
t.Fatal(err)
}

// create a new branch for testing tags in order to avoid disturbing the state
// of the current branch that's used for testing branches later.
err = createBranch(repo, "tag-testing")
if err != nil {
t.Fatal(err)
}
hash, err := commitFile(repo, "bar.txt", "this is the way", time.Now())
if err != nil {
t.Fatal(err)
}
err = repo.Push(&extgogit.PushOptions{})
if err != nil {
t.Fatal(err)
}
_, err = tag(repo, hash, false, "v0.1.0", time.Now())
if err != nil {
t.Fatal(err)
}
err = repo.Push(&extgogit.PushOptions{
RefSpecs: []config.RefSpec{
config.RefSpec("+refs/tags/v0.1.0" + ":refs/tags/v0.1.0"),
},
})
if err != nil {
t.Fatal(err)
}

// set a custom reference, in the format of GitHub PRs.
err = repo.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName("/refs/pull/1/head"), hash))
if err != nil {
t.Fatal(err)
}
err = repo.Push(&extgogit.PushOptions{
RefSpecs: []config.RefSpec{
config.RefSpec("+refs/pull/1/head" + ":refs/pull/1/head"),
},
})
if err != nil {
t.Fatal(err)
}

tests := []struct {
name string
refName string
filesCreated map[string]string
lastRevision string
expectedCommit string
expectedConcreteCommit bool
expectedErr string
}{
{
name: "ref name pointing to a branch",
refName: "refs/heads/master",
filesCreated: map[string]string{"foo.txt": "test file\n"},
expectedCommit: git.Hash(head.Hash().String()).Digest(),
expectedConcreteCommit: true,
},
{
name: "skip clone if LastRevision is unchanged",
refName: "refs/heads/master",
lastRevision: git.Hash(head.Hash().String()).Digest(),
expectedCommit: git.Hash(head.Hash().String()).Digest(),
expectedConcreteCommit: false,
},
{
name: "skip clone if LastRevision is unchanged even if the reference changes",
refName: "refs/heads/test",
lastRevision: git.Hash(head.Hash().String()).Digest(),
expectedCommit: git.Hash(head.Hash().String()).Digest(),
expectedConcreteCommit: false,
},
{
name: "ref name pointing to a tag",
refName: "refs/tags/v0.1.0",
filesCreated: map[string]string{"bar.txt": "this is the way"},
lastRevision: git.Hash(head.Hash().String()).Digest(),
expectedCommit: git.Hash(hash.String()).Digest(),
expectedConcreteCommit: true,
},
{
name: "ref name pointing to a pull request",
refName: "refs/pull/1/head",
filesCreated: map[string]string{"bar.txt": "this is the way"},
expectedCommit: git.Hash(hash.String()).Digest(),
expectedConcreteCommit: true,
},
{
name: "non existing ref",
refName: "refs/tags/v0.2.0",
expectedErr: "unable to resolve ref: refs/tags/v0.2.0 to a specific commit",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
tmpDir := t.TempDir()
ggc, err := NewClient(tmpDir, &git.AuthOptions{Transport: git.HTTP})
g.Expect(err).ToNot(HaveOccurred())

cc, err := ggc.Clone(context.TODO(), repoURL, repository.CloneOptions{
CheckoutStrategy: repository.CheckoutStrategy{
RefName: tt.refName,
},
LastObservedCommit: tt.lastRevision,
})

if tt.expectedErr != "" {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr))
g.Expect(cc).To(BeNil())
return
}

g.Expect(err).ToNot(HaveOccurred())
g.Expect(cc.String()).To(Equal(tt.expectedCommit))
g.Expect(git.IsConcreteCommit(*cc)).To(Equal(tt.expectedConcreteCommit))

if tt.expectedConcreteCommit {
for k, v := range tt.filesCreated {
g.Expect(filepath.Join(tmpDir, k)).To(BeARegularFile())
content, err := os.ReadFile(filepath.Join(tmpDir, k))
g.Expect(err).ToNot(HaveOccurred())
g.Expect(string(content)).To(Equal(v))
}
}
})
}
}

func Test_cloneSubmodule(t *testing.T) {
g := NewWithT(t)

Expand Down
10 changes: 8 additions & 2 deletions git/repository/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,16 @@ type CheckoutStrategy struct {
// Tag to checkout, takes precedence over Branch.
Tag string

// SemVer tag expression to checkout, takes precedence over Tag.
// SemVer tag expression to checkout, takes precedence over Branch and Tag.
SemVer string `json:"semver,omitempty"`

// Commit SHA1 to checkout, takes precedence over Tag and SemVer.
// RefName is the reference to checkout to. It must conform to the
// Git reference format: https://git-scm.com/book/en/v2/Git-Internals-Git-References
// Examples: "/refs/heads/main", "refs/pull/420/head", "refs/tags/v0.1.0"
// It takes precedence over Branch, Tag and SemVer.
RefName string

// Commit SHA1 to checkout, takes precedence over all the other options.
// If supported by the client, it can be combined with Branch.
Commit string
}
Expand Down

0 comments on commit 2faf04a

Please sign in to comment.