Skip to content

Commit

Permalink
git: add support for cloning empty repos
Browse files Browse the repository at this point in the history
Signed-off-by: Sanskar Jaiswal <[email protected]>
  • Loading branch information
aryan9600 committed Nov 8, 2022
1 parent 2ee90dd commit 2616f88
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 4 deletions.
2 changes: 2 additions & 0 deletions git/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
40 changes: 38 additions & 2 deletions git/gogit/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"fmt"
"io"
"os"
"sort"
"strings"
"time"
Expand Down Expand Up @@ -93,13 +94,48 @@ 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, init a new Git repo in that directory and then
// do a fresh clone.
if err == transport.ErrEmptyRemoteRepository {
if err = os.RemoveAll(g.path); err == nil {
if err = g.Init(ctx, url, branch); err == nil {
commit, err := g.Clone(ctx, url, opts)
if err == nil {
return commit, nil
}
}
}
}
if err != nil {
return nil, fmt.Errorf("unable to clone '%s': %w", url, gitutil.GoGitError(err))
}
}

// Fetch the log and count the entries to detect if the repository we cloned is empty.
// Is there a better way?
log, err := repo.Log(&extgogit.LogOptions{
All: true,
})
if err != nil {
return nil, fmt.Errorf("unable to resole Git repository log: %w", err)
}

var l int
log.ForEach(func(c *object.Commit) error {
l += 1
return nil
})
// Since there are no logs, the repo is empty, hence we exit early.
if l == 0 {
g.repository = repo
return nil, nil
}

head, err := repo.Head()
Expand Down
30 changes: 28 additions & 2 deletions git/gogit/clone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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
Expand All @@ -73,6 +78,7 @@ func TestClone_cloneBranch(t *testing.T) {
expectedCommit string
expectedConcreteCommit bool
expectedErr string
expectedEmpty bool
}{
{
name: "Default branch",
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
},
Expand All @@ -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))
Expand Down
8 changes: 8 additions & 0 deletions git/libgit2/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down

0 comments on commit 2616f88

Please sign in to comment.