diff --git a/source/git/source.go b/source/git/source.go index e9fc50320..67a3b13e1 100644 --- a/source/git/source.go +++ b/source/git/source.go @@ -500,8 +500,8 @@ func (gs *gitSourceHandler) Snapshot(ctx context.Context, g session.Group) (cach ref, err := gs.trySnapshot(ctx, g, false) if err != nil { var wce *wouldClobberExistingTagError - if errors.As(err, &wce) { - // try once more with a fresh repo + var ulre *unableToUpdateLocalRefError + if errors.As(err, &wce) || errors.As(err, &ulre) { ref, err = gs.trySnapshot(ctx, g, true) if err != nil { return nil, err @@ -581,6 +581,11 @@ func (gs *gitSourceHandler) trySnapshot(ctx context.Context, g session.Group, re // only hope is to abandon the existing shared repo and start a fresh one return nil, &wouldClobberExistingTagError{err} } + if strings.Contains(err.Error(), "(unable to update local ref)") && strings.Contains(err.Error(), "some local refs could not be updated;") { + // this can happen if a branch updated in remote so that old branch + // is now a parent dir of a new branch + return nil, &unableToUpdateLocalRefError{err} + } return nil, err } @@ -794,6 +799,14 @@ func (e *wouldClobberExistingTagError) Unwrap() error { return e.error } +type unableToUpdateLocalRefError struct { + error +} + +func (e *unableToUpdateLocalRefError) Unwrap() error { + return e.error +} + func (gs *gitSourceHandler) emptyGitCli(ctx context.Context, g session.Group, opts ...gitutil.Option) (*gitutil.GitCLI, func() error, error) { var cleanups []func() error cleanup := func() error { diff --git a/source/git/source_test.go b/source/git/source_test.go index 98821cd8b..e7b29954a 100644 --- a/source/git/source_test.go +++ b/source/git/source_test.go @@ -915,6 +915,89 @@ func testFetchMutatedTag(t *testing.T, format string, keepGitDir bool) { ref.Release(context.TODO()) } +func TestFetchMutatedBranchSHA1(t *testing.T) { + testFetchMutatedBranch(t, "sha1", false) +} + +func TestFetchMutatedBranchKeepGitDirSHA1(t *testing.T) { + testFetchMutatedBranch(t, "sha1", true) +} + +func TestFetchMutatedBranchSHA256(t *testing.T) { + testFetchMutatedBranch(t, "sha256", false) +} + +func TestFetchMutatedBranchKeepGitDirSHA256(t *testing.T) { + testFetchMutatedBranch(t, "sha256", true) +} + +// testFetchMutatedBranch tests that if a branch is mutated in a way that previous +// ref becomes parent directory of new ref, causing collision to existing checkouts +func testFetchMutatedBranch(t *testing.T, format string, keepGitDir bool) { + ctx := t.Context() + if runtime.GOOS == "windows" { + t.Skip("Depends on unimplemented containerd bind-mount support on Windows") + } + repo := setupGitRepo(t, format) + cmd := exec.Command("git", "rev-parse", "feature") + cmd.Dir = repo.mainPath + + out, err := cmd.Output() + require.NoError(t, err) + + expLen := 40 + if format == "sha256" { + expLen = 64 + } + shaBranch := strings.TrimSpace(string(out)) + require.Equal(t, expLen, len(shaBranch)) + + id := &GitIdentifier{Remote: repo.mainURL, Ref: "feature", KeepGitDir: keepGitDir} + gs := setupGitSource(t, t.TempDir()) + + g, err := gs.Resolve(ctx, id, nil, nil) + require.NoError(t, err) + + key1, pin1, _, done, err := g.CacheKey(ctx, nil, 0) + require.NoError(t, err) + require.True(t, done) + + ref, err := g.Snapshot(ctx, nil) + require.NoError(t, err) + ref.Release(context.TODO()) + + // mutate the branch to point to become parent dir + cmd = exec.Command("git", "branch", "-D", "feature") + cmd.Dir = repo.mainPath + out, err = cmd.CombinedOutput() + require.NoError(t, err, string(out)) + + cmd = exec.Command("git", "branch", "feature/new", shaBranch) + cmd.Dir = repo.mainPath + out, err = cmd.CombinedOutput() + require.NoError(t, err, string(out)) + + id = &GitIdentifier{Remote: repo.mainURL, Ref: "feature/new", KeepGitDir: keepGitDir} + + g, err = gs.Resolve(ctx, id, nil, nil) + require.NoError(t, err) + + key2, pin2, _, done, err := g.CacheKey(ctx, nil, 0) + require.NoError(t, err) + require.True(t, done) + + if keepGitDir { + require.NotEqual(t, key1, key2) // key contains new ref + } else { + require.Equal(t, key1, key2) + } + require.Equal(t, pin1, pin2) + + ref, err = g.Snapshot(ctx, nil) + require.NoError(t, err) + ref.Release(context.TODO()) +} + func TestMultipleTagAccessKeepGitDirSHA1(t *testing.T) { testMultipleTagAccess(t, true, "sha1") }