diff --git a/pkg/git/libgit2/checkout.go b/pkg/git/libgit2/checkout.go index 261ee1ebb..a4a5721a3 100644 --- a/pkg/git/libgit2/checkout.go +++ b/pkg/git/libgit2/checkout.go @@ -161,25 +161,50 @@ func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *g } defer upstreamCommit.Free() - // Once the index has been updated with Fetch, and we know the tip commit, - // a hard reset can be used to align the local worktree with the remote branch's. - err = repo.ResetToCommit(upstreamCommit, git2go.ResetHard, &git2go.CheckoutOptions{ + // We try to lookup the branch (and create it if it doesn't exist), so that we can + // switch the repo to the specified branch. This is done so that users of this api + // can expect the repo to be at the desired branch, when cloned. + localBranch, err := repo.LookupBranch(c.Branch, git2go.BranchLocal) + if git2go.IsErrorCode(err, git2go.ErrorCodeNotFound) { + localBranch, err = repo.CreateBranch(c.Branch, upstreamCommit, false) + if err != nil { + return nil, fmt.Errorf("unable to create local branch '%s': %w", c.Branch, err) + } + } else if err != nil { + return nil, fmt.Errorf("unable to lookup branch '%s': %w", c.Branch, err) + } + defer localBranch.Free() + + tree, err := repo.LookupTree(upstreamCommit.TreeId()) + if err != nil { + return nil, fmt.Errorf("unable to lookup tree for branch '%s': %w", c.Branch, err) + } + defer tree.Free() + + err = repo.CheckoutTree(tree, &git2go.CheckoutOpts{ + // the remote branch should take precedence if it exists at this point in time. Strategy: git2go.CheckoutForce, }) if err != nil { - return nil, fmt.Errorf("unable to hard reset to commit for '%s': %w", managed.EffectiveURL(url), gitutil.LibGit2Error(err)) + return nil, fmt.Errorf("unable to checkout tree for branch '%s': %w", c.Branch, err) + } + + // Set the current head to point to the requested branch. + err = repo.SetHead("refs/heads/" + c.Branch) + if err != nil { + return nil, fmt.Errorf("unable to set HEAD to branch '%s':%w", c.Branch, err) } // Use the current worktree's head as reference for the commit to be returned. head, err := repo.Head() if err != nil { - return nil, fmt.Errorf("git resolve HEAD error: %w", err) + return nil, fmt.Errorf("unable to resolve HEAD: %w", err) } defer head.Free() cc, err := repo.LookupCommit(head.Target()) if err != nil { - return nil, fmt.Errorf("failed to lookup HEAD commit '%s' for branch '%s': %w", head.Target(), c.Branch, err) + return nil, fmt.Errorf("unable to lookup HEAD commit '%s' for branch '%s': %w", head.Target(), c.Branch, err) } defer cc.Free() diff --git a/pkg/git/libgit2/managed_test.go b/pkg/git/libgit2/managed_test.go index 8d05692e5..cf5aabc56 100644 --- a/pkg/git/libgit2/managed_test.go +++ b/pkg/git/libgit2/managed_test.go @@ -517,6 +517,7 @@ func TestManagedCheckoutBranch_Checkout(t *testing.T) { repo, err := git2go.OpenRepository(filepath.Join(server.Root(), repoPath)) g.Expect(err).ToNot(HaveOccurred()) + defer repo.Free() branchRef, err := repo.References.Lookup(fmt.Sprintf("refs/heads/%s", git.DefaultBranch)) g.Expect(err).ToNot(HaveOccurred()) @@ -524,6 +525,7 @@ func TestManagedCheckoutBranch_Checkout(t *testing.T) { commit, err := repo.LookupCommit(branchRef.Target()) g.Expect(err).ToNot(HaveOccurred()) + defer commit.Free() authOpts := &git.AuthOptions{ TransportOptionsURL: getTransportOptionsURL(git.HTTP), @@ -552,6 +554,33 @@ func TestManagedCheckoutBranch_Checkout(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) g.Expect(cc.String()).To(Equal(git.DefaultBranch + "/" + commit.Id().String())) g.Expect(git.IsConcreteCommit(*cc)).To(Equal(true)) + + // Create a new branch and push it. + err = createBranch(repo, "test", nil) + g.Expect(err).ToNot(HaveOccurred()) + transportOptsURL := getTransportOptionsURL(git.HTTP) + managed.AddTransportOptions(transportOptsURL, managed.TransportOptions{ + TargetURL: repoURL, + }) + defer managed.RemoveTransportOptions(transportOptsURL) + origin, err := repo.Remotes.Create("origin", transportOptsURL) + defer origin.Free() + g.Expect(err).ToNot(HaveOccurred()) + err = origin.Push([]string{"refs/heads/test:refs/heads/test"}, &git2go.PushOptions{}) + g.Expect(err).ToNot(HaveOccurred()) + + branch.Branch = "test" + tmpDir2 := t.TempDir() + cc, err = branch.Checkout(ctx, tmpDir2, repoURL, authOpts) + g.Expect(err).ToNot(HaveOccurred()) + + // Check if the repo HEAD points to the branch. + repo, err = git2go.OpenRepository(tmpDir2) + g.Expect(err).ToNot(HaveOccurred()) + head, err := repo.Head() + defer head.Free() + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(head.Branch().Name()).To(Equal("test")) } func TestManagedCheckoutTag_Checkout(t *testing.T) { @@ -573,6 +602,7 @@ func TestManagedCheckoutTag_Checkout(t *testing.T) { repo, err := git2go.OpenRepository(filepath.Join(server.Root(), repoPath)) g.Expect(err).ToNot(HaveOccurred()) + defer repo.Free() branchRef, err := repo.References.Lookup(fmt.Sprintf("refs/heads/%s", git.DefaultBranch)) g.Expect(err).ToNot(HaveOccurred()) @@ -580,6 +610,7 @@ func TestManagedCheckoutTag_Checkout(t *testing.T) { commit, err := repo.LookupCommit(branchRef.Target()) g.Expect(err).ToNot(HaveOccurred()) + defer commit.Free() _, err = tag(repo, commit.Id(), false, "tag-1", time.Now()) checkoutTag := CheckoutTag{