Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions pkg/git/checkout_remote_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package v2

import (
"context"
"os"
"path"
"testing"
"time"

gogit "github.com/go-git/go-git/v5"
gogitconfig "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/stretchr/testify/require"
)

func seedCommit(t *testing.T, originDir, fileContent, pushRefSpec string) plumbing.Hash {
t.Helper()

seed := t.TempDir()
repo, err := gogit.PlainInit(seed, false)
require.NoError(t, err)

require.NoError(t, os.WriteFile(path.Join(seed, "f.txt"), []byte(fileContent), 0o600))

wt, err := repo.Worktree()
require.NoError(t, err)

_, err = wt.Add("f.txt")
require.NoError(t, err)

hash, err := wt.Commit("init "+fileContent, &gogit.CommitOptions{
Author: &object.Signature{Name: "t", Email: "t@t", When: time.Now()},
})
require.NoError(t, err)

_, err = repo.CreateRemote(&gogitconfig.RemoteConfig{Name: "origin", URLs: []string{originDir}})
require.NoError(t, err)
require.NoError(t, repo.Push(&gogit.PushOptions{
RemoteName: "origin",
RefSpecs: []gogitconfig.RefSpec{gogitconfig.RefSpec(pushRefSpec)},
Force: true,
}))

return hash
}

func buildOriginWithClone(t *testing.T) (string, string) {
t.Helper()

originDir := t.TempDir()
_, err := gogit.PlainInit(originDir, true)
require.NoError(t, err)

seedCommit(t, originDir, "base", "+refs/heads/master:refs/heads/master")

workDir := path.Join(t.TempDir(), "clone")
gp := NewGitProvider(Config{})
require.NoError(t, gp.Clone(context.Background(), originDir, workDir))

return originDir, workDir
}

func workdirHead(t *testing.T, workDir string) *plumbing.Reference {
t.Helper()

repo, err := gogit.PlainOpen(workDir)
require.NoError(t, err)

head, err := repo.Head()
require.NoError(t, err)

return head
}

// A branch pushed to origin after the workdir was cloned is fetched into
// refs/heads by Checkout's refs/*:refs/* refspec; the existence check must look
// there, or the checkout collides on Create with the ref the fetch just wrote.
func TestCheckout_RemoteBranchCreatedAfterClone(t *testing.T) {
originDir, workDir := buildOriginWithClone(t)

origin, err := gogit.PlainOpen(originDir)
require.NoError(t, err)

masterRef, err := origin.Reference(plumbing.NewBranchReferenceName("master"), false)
require.NoError(t, err)
require.NoError(t, origin.Storer.SetReference(
plumbing.NewHashReference(plumbing.NewBranchReferenceName("late-branch"), masterRef.Hash()),
))

gp := NewGitProvider(Config{})
err = gp.Checkout(context.Background(), workDir, "late-branch", true)
require.NoError(t, err, "checkout of a branch created after clone must succeed")

require.Equal(t, "late-branch", workdirHead(t, workDir).Name().Short())
}

// TestCheckout_RemoteBranchPresentAtCloneTime covers the branchToCopy flow in
// its common shape: the target branch existed when the workdir was cloned.
func TestCheckout_RemoteBranchPresentAtCloneTime(t *testing.T) {
originDir := t.TempDir()
_, err := gogit.PlainInit(originDir, true)
require.NoError(t, err)

seedCommit(t, originDir, "base", "+refs/heads/master:refs/heads/master")
seedCommit(t, originDir, "base", "+refs/heads/master:refs/heads/feature")

workDir := path.Join(t.TempDir(), "clone")
gp := NewGitProvider(Config{})
require.NoError(t, gp.Clone(context.Background(), originDir, workDir))

require.NoError(t, gp.Checkout(context.Background(), workDir, "feature", true))
require.Equal(t, "feature", workdirHead(t, workDir).Name().Short())
}

// TestCheckout_BranchAbsentEverywhere preserves the create-fallback: a branch
// that exists neither locally nor on the remote is created from HEAD.
func TestCheckout_BranchAbsentEverywhere(t *testing.T) {
_, workDir := buildOriginWithClone(t)

gp := NewGitProvider(Config{})
require.NoError(t, gp.Checkout(context.Background(), workDir, "brand-new", true))
require.Equal(t, "brand-new", workdirHead(t, workDir).Name().Short())
}

// TestCheckout_ForcePushedRemoteBranch: a rewritten upstream branch must not
// fail the fetch of a cached workdir; the checkout lands on the new tip.
func TestCheckout_ForcePushedRemoteBranch(t *testing.T) {
originDir, workDir := buildOriginWithClone(t)

rewritten := seedCommit(t, originDir, "rewritten-history", "+refs/heads/master:refs/heads/master")

gp := NewGitProvider(Config{})
require.NoError(t, gp.Checkout(context.Background(), workDir, "master", true),
"force-pushed upstream branch must not fail the checkout fetch")
require.Equal(t, rewritten, workdirHead(t, workDir).Hash())
}
15 changes: 9 additions & 6 deletions pkg/git/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,6 @@ func (p *GitProvider) Checkout(ctx context.Context, directory, branchName string
createBranch := true

if remote {
// Fetch from remote first
auth, err := p.getAuth()
if err != nil {
return fmt.Errorf("failed to get authentication: %w", err)
Expand All @@ -325,19 +324,23 @@ func (p *GitProvider) Checkout(ctx context.Context, directory, branchName string
RefSpecs: []config.RefSpec{"refs/*:refs/*"},
Auth: auth,
Progress: os.Stdout,
// The refspec has no force prefix, so without this a rebased or
// force-pushed upstream branch fails the fetch with ErrForceNeeded
// against a cached workdir.
Force: true,
}

err = repo.FetchContext(ctx, fetchOptions)
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return fmt.Errorf("failed to fetch: %w", err)
}

// Check if branch exists remotely
remoteBranchRef := plumbing.NewRemoteReferenceName("origin", branchName)

_, err = repo.Reference(remoteBranchRef, false)
// The refspec above maps remote branches straight into local
// refs/heads, so that is the namespace that proves existence here.
// Checking refs/remotes/origin instead would miss any branch the
// fetch just materialized and collide on Create below.
_, err = repo.Reference(plumbing.NewBranchReferenceName(branchName), false)
if err == nil {
// Branch exists remotely, don't create locally
createBranch = false
}
}
Expand Down
Loading