diff --git a/git/client.go b/git/client.go index f7f3daa0..e9d96abb 100644 --- a/git/client.go +++ b/git/client.go @@ -24,6 +24,8 @@ import ( // on a Git repository. type RepositoryReader interface { // Clone clones a repository from the provided url using the options provided. + // It returns a Commit object describing the Git commit that the repository + // HEAD points to. If the repository is empty, it returns a nil Commit. Clone(ctx context.Context, url string, cloneOpts CloneOptions) (*Commit, error) // IsClean returns whether the working tree is clean. IsClean() (bool, error) diff --git a/git/gogit/clone.go b/git/gogit/clone.go index 032c3053..b615becc 100644 --- a/git/gogit/clone.go +++ b/git/gogit/clone.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "io" + "os" "sort" "strings" "time" @@ -93,13 +94,25 @@ func (g *Client) cloneBranch(ctx context.Context, url, branch string, opts git.C repo, err := extgogit.CloneContext(ctx, g.storer, g.worktreeFS, cloneOpts) if err != nil { - if err == transport.ErrEmptyRemoteRepository || err == transport.ErrRepositoryNotFound || isRemoteBranchNotFoundErr(err, ref.String()) { + if err == transport.ErrRepositoryNotFound || isRemoteBranchNotFoundErr(err, ref.String()) { return nil, git.ErrRepositoryNotFound{ Message: fmt.Sprintf("unable to clone: %s", err), URL: url, } } - return nil, fmt.Errorf("unable to clone '%s': %w", url, gitutil.GoGitError(err)) + // Directly cloning an empty Git repo to a directory fails with this error. + // We check for the error and then init a new Git repo in that directory + // (which represents an empty repository). + if err == transport.ErrEmptyRemoteRepository { + if err = os.RemoveAll(g.path); err == nil { + if err = g.Init(ctx, url, branch); err == nil { + return nil, nil + } + } + } + if err != nil { + return nil, fmt.Errorf("unable to clone '%s': %w", url, gitutil.GoGitError(err)) + } } head, err := repo.Head() diff --git a/git/gogit/clone_test.go b/git/gogit/clone_test.go index e11289d2..3ba5b8d0 100644 --- a/git/gogit/clone_test.go +++ b/git/gogit/clone_test.go @@ -46,7 +46,7 @@ import ( const testRepositoryPath = "../testdata/git/repo" func TestClone_cloneBranch(t *testing.T) { - repo, path, err := initRepo(t) + repo, repoPath, err := initRepo(t) if err != nil { t.Fatal(err) } @@ -65,6 +65,11 @@ func TestClone_cloneBranch(t *testing.T) { t.Fatal(err) } + _, emptyRepoPath, err := initRepo(t) + if err != nil { + t.Fatal(err) + } + tests := []struct { name string branch string @@ -73,6 +78,7 @@ func TestClone_cloneBranch(t *testing.T) { expectedCommit string expectedConcreteCommit bool expectedErr string + expectedEmpty bool }{ { name: "Default branch", @@ -102,6 +108,11 @@ func TestClone_cloneBranch(t *testing.T) { branch: "invalid", expectedErr: "couldn't find remote ref \"refs/heads/invalid\"", }, + { + name: "empty repo", + branch: "master", + expectedEmpty: true, + }, } for _, tt := range tests { @@ -112,7 +123,14 @@ func TestClone_cloneBranch(t *testing.T) { ggc, err := NewClient(tmpDir, &git.AuthOptions{Transport: git.HTTP}) g.Expect(err).ToNot(HaveOccurred()) - cc, err := ggc.Clone(context.TODO(), path, git.CloneOptions{ + var upstreamPath string + if tt.expectedEmpty { + upstreamPath = emptyRepoPath + } else { + upstreamPath = repoPath + } + + cc, err := ggc.Clone(context.TODO(), upstreamPath, git.CloneOptions{ CheckoutStrategy: git.CheckoutStrategy{ Branch: tt.branch, }, @@ -125,6 +143,14 @@ func TestClone_cloneBranch(t *testing.T) { g.Expect(cc).To(BeNil()) return } + + if tt.expectedEmpty { + g.Expect(cc).To(BeNil()) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(filepath.Join(ggc.path, ".git")).To(BeADirectory()) + return + } + g.Expect(err).ToNot(HaveOccurred()) g.Expect(cc.String()).To(Equal(tt.branch + "/" + tt.expectedCommit)) g.Expect(git.IsConcreteCommit(*cc)).To(Equal(tt.expectedConcreteCommit)) diff --git a/git/libgit2/clone.go b/git/libgit2/clone.go index 958b0877..00be13d3 100644 --- a/git/libgit2/clone.go +++ b/git/libgit2/clone.go @@ -79,6 +79,14 @@ func (l *Client) cloneBranch(ctx context.Context, url, branch string, opts git.C return nil, fmt.Errorf("unable to fetch remote '%s': %w", url, gitutil.LibGit2Error(err)) } + isEmpty, err := l.repository.IsEmpty() + if err != nil { + return nil, fmt.Errorf("unable to check if cloned repo '%s' is empty: %w", url, err) + } + if isEmpty { + return nil, nil + } + branchRef, err := l.repository.References.Lookup(fmt.Sprintf("refs/remotes/origin/%s", branch)) if err != nil { return nil, fmt.Errorf("unable to lookup branch '%s' for '%s': %w", branch, url, gitutil.LibGit2Error(err))