Skip to content

Commit e8e803e

Browse files
authored
Merge pull request #465 from aryan9600/clone-refname
add support for checking out git repo to a ref
2 parents 2bb3aa8 + ca1dce0 commit e8e803e

File tree

5 files changed

+204
-2
lines changed

5 files changed

+204
-2
lines changed

git/gogit/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ func (g *Client) Clone(ctx context.Context, url string, cloneOpts repository.Clo
226226
switch {
227227
case checkoutStrat.Commit != "":
228228
return g.cloneCommit(ctx, url, checkoutStrat.Commit, cloneOpts)
229+
case checkoutStrat.RefName != "":
230+
return g.cloneRefName(ctx, url, checkoutStrat.RefName, cloneOpts)
229231
case checkoutStrat.Tag != "":
230232
return g.cloneTag(ctx, url, checkoutStrat.Tag, cloneOpts)
231233
case checkoutStrat.SemVer != "":

git/gogit/clone.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ func (g *Client) cloneBranch(ctx context.Context, url, branch string, opts repos
5555
return nil, err
5656
}
5757
if head != "" && head == lastObserved {
58+
// Construct a non-concrete commit with the existing information.
5859
c := &git.Commit{
5960
Hash: git.ExtractHashFromRevision(head),
6061
Reference: plumbing.NewBranchReferenceName(branch).String(),
@@ -134,6 +135,7 @@ func (g *Client) cloneTag(ctx context.Context, url, tag string, opts repository.
134135
return nil, err
135136
}
136137
if head != "" && head == lastObserved {
138+
// Construct a non-concrete commit with the existing information.
137139
c := &git.Commit{
138140
Hash: git.ExtractHashFromRevision(head),
139141
Reference: ref.String(),
@@ -354,6 +356,39 @@ func (g *Client) cloneSemVer(ctx context.Context, url, semverTag string, opts re
354356
return buildCommitWithRef(cc, ref)
355357
}
356358

359+
func (g *Client) cloneRefName(ctx context.Context, url string, refName string, cloneOpts repository.CloneOptions) (*git.Commit, error) {
360+
if g.authOpts == nil {
361+
return nil, fmt.Errorf("unable to checkout repo with an empty set of auth options")
362+
}
363+
authMethod, err := transportAuth(g.authOpts, g.useDefaultKnownHosts)
364+
if err != nil {
365+
return nil, fmt.Errorf("unable to construct auth method with options: %w", err)
366+
}
367+
head, err := getRemoteHEAD(ctx, url, plumbing.ReferenceName(refName), g.authOpts, authMethod)
368+
if err != nil {
369+
return nil, err
370+
}
371+
if head == "" {
372+
return nil, fmt.Errorf("unable to resolve ref '%s' to a specific commit", refName)
373+
}
374+
375+
hash := git.ExtractHashFromRevision(head)
376+
// check if previous revision has changed before attempting to clone
377+
if lastObserved := git.TransformRevision(cloneOpts.LastObservedCommit); lastObserved != "" {
378+
if hash.Digest() != "" && hash.Digest() == lastObserved {
379+
// Construct a non-concrete commit with the existing information.
380+
// We exclude the reference here to ensure compatibility with the format
381+
// of the Commit object returned by cloneCommit().
382+
c := &git.Commit{
383+
Hash: hash,
384+
}
385+
return c, nil
386+
}
387+
}
388+
389+
return g.cloneCommit(ctx, url, hash.String(), cloneOpts)
390+
}
391+
357392
func recurseSubmodules(recurse bool) extgogit.SubmoduleRescursivity {
358393
if recurse {
359394
return extgogit.DefaultSubmoduleRecursionDepth
@@ -363,6 +398,11 @@ func recurseSubmodules(recurse bool) extgogit.SubmoduleRescursivity {
363398

364399
func getRemoteHEAD(ctx context.Context, url string, ref plumbing.ReferenceName,
365400
authOpts *git.AuthOptions, authMethod transport.AuthMethod) (string, error) {
401+
// ref: https://git-scm.com/docs/git-check-ref-format#_description; point no. 6
402+
if strings.HasPrefix(ref.String(), "/") || strings.HasSuffix(ref.String(), "/") {
403+
return "", fmt.Errorf("ref %s is invalid; Git refs cannot begin or end with a slash '/'", ref.String())
404+
}
405+
366406
remoteCfg := &config.RemoteConfig{
367407
Name: git.DefaultRemote,
368408
URLs: []string{url},

git/gogit/clone_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"time"
3333

3434
extgogit "github.com/fluxcd/go-git/v5"
35+
"github.com/fluxcd/go-git/v5/config"
3536
"github.com/fluxcd/go-git/v5/plumbing"
3637
"github.com/fluxcd/go-git/v5/plumbing/cache"
3738
"github.com/fluxcd/go-git/v5/plumbing/object"
@@ -493,6 +494,147 @@ func TestClone_cloneSemVer(t *testing.T) {
493494
}
494495
}
495496

497+
func TestClone_cloneRefName(t *testing.T) {
498+
g := NewWithT(t)
499+
500+
server, err := gittestserver.NewTempGitServer()
501+
g.Expect(err).ToNot(HaveOccurred())
502+
defer os.RemoveAll(server.Root())
503+
err = server.StartHTTP()
504+
g.Expect(err).ToNot(HaveOccurred())
505+
defer server.StopHTTP()
506+
507+
repoPath := "test.git"
508+
err = server.InitRepo("../testdata/git/repo", git.DefaultBranch, repoPath)
509+
g.Expect(err).ToNot(HaveOccurred())
510+
repoURL := server.HTTPAddress() + "/" + repoPath
511+
repo, err := extgogit.PlainClone(t.TempDir(), false, &extgogit.CloneOptions{
512+
URL: repoURL,
513+
})
514+
g.Expect(err).ToNot(HaveOccurred())
515+
516+
// head is the current HEAD on master
517+
head, err := repo.Head()
518+
g.Expect(err).ToNot(HaveOccurred())
519+
err = createBranch(repo, "test")
520+
g.Expect(err).ToNot(HaveOccurred())
521+
err = repo.Push(&extgogit.PushOptions{})
522+
g.Expect(err).ToNot(HaveOccurred())
523+
524+
// create a new branch for testing tags in order to avoid disturbing the state
525+
// of the current branch that's used for testing branches later.
526+
err = createBranch(repo, "tag-testing")
527+
g.Expect(err).ToNot(HaveOccurred())
528+
hash, err := commitFile(repo, "bar.txt", "this is the way", time.Now())
529+
g.Expect(err).ToNot(HaveOccurred())
530+
err = repo.Push(&extgogit.PushOptions{})
531+
g.Expect(err).ToNot(HaveOccurred())
532+
_, err = tag(repo, hash, false, "v0.1.0", time.Now())
533+
g.Expect(err).ToNot(HaveOccurred())
534+
err = repo.Push(&extgogit.PushOptions{
535+
RefSpecs: []config.RefSpec{
536+
config.RefSpec("+refs/tags/v0.1.0" + ":refs/tags/v0.1.0"),
537+
},
538+
})
539+
g.Expect(err).ToNot(HaveOccurred())
540+
541+
// set a custom reference, in the format of GitHub PRs.
542+
err = repo.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName("/refs/pull/1/head"), hash))
543+
g.Expect(err).ToNot(HaveOccurred())
544+
err = repo.Push(&extgogit.PushOptions{
545+
RefSpecs: []config.RefSpec{
546+
config.RefSpec("+refs/pull/1/head" + ":refs/pull/1/head"),
547+
},
548+
})
549+
g.Expect(err).ToNot(HaveOccurred())
550+
551+
tests := []struct {
552+
name string
553+
refName string
554+
filesCreated map[string]string
555+
lastRevision string
556+
expectedCommit string
557+
expectedConcreteCommit bool
558+
expectedErr string
559+
}{
560+
{
561+
name: "ref name pointing to a branch",
562+
refName: "refs/heads/master",
563+
filesCreated: map[string]string{"foo.txt": "test file\n"},
564+
expectedCommit: git.Hash(head.Hash().String()).Digest(),
565+
expectedConcreteCommit: true,
566+
},
567+
{
568+
name: "skip clone if LastRevision is unchanged",
569+
refName: "refs/heads/master",
570+
lastRevision: git.Hash(head.Hash().String()).Digest(),
571+
expectedCommit: git.Hash(head.Hash().String()).Digest(),
572+
expectedConcreteCommit: false,
573+
},
574+
{
575+
name: "skip clone if LastRevision is unchanged even if the reference changes",
576+
refName: "refs/heads/test",
577+
lastRevision: git.Hash(head.Hash().String()).Digest(),
578+
expectedCommit: git.Hash(head.Hash().String()).Digest(),
579+
expectedConcreteCommit: false,
580+
},
581+
{
582+
name: "ref name pointing to a tag",
583+
refName: "refs/tags/v0.1.0",
584+
filesCreated: map[string]string{"bar.txt": "this is the way"},
585+
lastRevision: git.Hash(head.Hash().String()).Digest(),
586+
expectedCommit: git.Hash(hash.String()).Digest(),
587+
expectedConcreteCommit: true,
588+
},
589+
{
590+
name: "ref name pointing to a pull request",
591+
refName: "refs/pull/1/head",
592+
filesCreated: map[string]string{"bar.txt": "this is the way"},
593+
expectedCommit: git.Hash(hash.String()).Digest(),
594+
expectedConcreteCommit: true,
595+
},
596+
{
597+
name: "non existing ref",
598+
refName: "refs/tags/v0.2.0",
599+
expectedErr: "unable to resolve ref 'refs/tags/v0.2.0' to a specific commit",
600+
},
601+
}
602+
603+
for _, tt := range tests {
604+
t.Run(tt.name, func(t *testing.T) {
605+
g := NewWithT(t)
606+
tmpDir := t.TempDir()
607+
ggc, err := NewClient(tmpDir, &git.AuthOptions{Transport: git.HTTP})
608+
g.Expect(err).ToNot(HaveOccurred())
609+
610+
cc, err := ggc.Clone(context.TODO(), repoURL, repository.CloneOptions{
611+
CheckoutStrategy: repository.CheckoutStrategy{
612+
RefName: tt.refName,
613+
},
614+
LastObservedCommit: tt.lastRevision,
615+
})
616+
617+
if tt.expectedErr != "" {
618+
g.Expect(err).To(HaveOccurred())
619+
g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr))
620+
g.Expect(cc).To(BeNil())
621+
return
622+
}
623+
624+
g.Expect(err).ToNot(HaveOccurred())
625+
g.Expect(cc.String()).To(Equal(tt.expectedCommit))
626+
g.Expect(git.IsConcreteCommit(*cc)).To(Equal(tt.expectedConcreteCommit))
627+
628+
for k, v := range tt.filesCreated {
629+
g.Expect(filepath.Join(tmpDir, k)).To(BeARegularFile())
630+
content, err := os.ReadFile(filepath.Join(tmpDir, k))
631+
g.Expect(err).ToNot(HaveOccurred())
632+
g.Expect(string(content)).To(Equal(v))
633+
}
634+
})
635+
}
636+
}
637+
496638
func Test_cloneSubmodule(t *testing.T) {
497639
g := NewWithT(t)
498640

@@ -997,6 +1139,16 @@ func Test_getRemoteHEAD(t *testing.T) {
9971139
head, err = getRemoteHEAD(context.TODO(), path, ref, &git.AuthOptions{}, nil)
9981140
g.Expect(err).ToNot(HaveOccurred())
9991141
g.Expect(head).To(Equal(fmt.Sprintf("%s@%s", "v0.1.0", git.Hash(cc.String()).Digest())))
1142+
1143+
ref = plumbing.ReferenceName("/refs/heads/main")
1144+
head, err = getRemoteHEAD(context.TODO(), path, ref, &git.AuthOptions{}, nil)
1145+
g.Expect(err).To(HaveOccurred())
1146+
g.Expect(err.Error()).To(Equal(fmt.Sprintf("ref %s is invalid; Git refs cannot begin or end with a slash '/'", ref.String())))
1147+
1148+
ref = plumbing.ReferenceName("refs/heads/main/")
1149+
head, err = getRemoteHEAD(context.TODO(), path, ref, &git.AuthOptions{}, nil)
1150+
g.Expect(err).To(HaveOccurred())
1151+
g.Expect(err.Error()).To(Equal(fmt.Sprintf("ref %s is invalid; Git refs cannot begin or end with a slash '/'", ref.String())))
10001152
}
10011153

10021154
func TestClone_CredentialsOverHttp(t *testing.T) {

git/libgit2/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ func (l *Client) Clone(ctx context.Context, url string, cloneOpts repository.Clo
193193
switch {
194194
case checkoutStrat.Commit != "":
195195
return l.cloneCommit(ctx, url, checkoutStrat.Commit, cloneOpts)
196+
case checkoutStrat.RefName != "":
197+
return nil, errors.New("unable to use RefName: client does not support this strategy")
196198
case checkoutStrat.Tag != "":
197199
return l.cloneTag(ctx, url, checkoutStrat.Tag, cloneOpts)
198200
case checkoutStrat.SemVer != "":

git/repository/options.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,16 @@ type CheckoutStrategy struct {
6262
// Tag to checkout, takes precedence over Branch.
6363
Tag string
6464

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

68-
// Commit SHA1 to checkout, takes precedence over Tag and SemVer.
68+
// RefName is the reference to checkout to. It must conform to the
69+
// Git reference format: https://git-scm.com/book/en/v2/Git-Internals-Git-References
70+
// Examples: "refs/heads/main", "refs/pull/420/head", "refs/tags/v0.1.0"
71+
// It takes precedence over Branch, Tag and SemVer.
72+
RefName string
73+
74+
// Commit SHA1 to checkout, takes precedence over all the other options.
6975
// If supported by the client, it can be combined with Branch.
7076
Commit string
7177
}

0 commit comments

Comments
 (0)