fix: accept case-aliased worktree repository paths (#3837)

This commit is contained in:
João Salavisa
2026-09-21 09:56:05 +01:00
committed by GitHub
parent 1614720ceb
commit db8131436f
4 changed files with 220 additions and 4 deletions
@@ -218,7 +218,16 @@ func validateLocalRepositoryWorkspace(ctx context.Context, workspacePath, reposi
if err != nil {
return err
}
if workspaceCommonDir != repositoryCommonDir {
workspaceCommonInfo, err := os.Stat(workspaceCommonDir)
if err != nil {
return worktree.ErrReuseWorktreeUnavailable
}
repositoryCommonInfo, err := os.Stat(repositoryCommonDir)
if err != nil {
return worktree.ErrReuseWorktreeUnavailable
}
if !workspaceCommonInfo.IsDir() || !repositoryCommonInfo.IsDir() ||
!os.SameFile(workspaceCommonInfo, repositoryCommonInfo) {
return worktree.ErrReuseWorktreeUnavailable
}
return nil
@@ -243,9 +252,9 @@ func localGitTopLevel(ctx context.Context, path string) (string, error) {
}
// localGitCommonDir returns the canonical Git directory shared by a checkout
// and all of its linked worktrees. Comparing this value, instead of the
// worktree top-level paths, proves that a linked worktree belongs to the
// selected repository while still rejecting an unrelated Git checkout.
// and all of its linked worktrees. Its filesystem identity, rather than its
// path spelling, proves that a linked worktree belongs to the selected
// repository while still rejecting an unrelated Git checkout.
func localGitCommonDir(ctx context.Context, path string) (string, error) {
info, err := os.Stat(path)
if err != nil || !info.IsDir() {
@@ -78,6 +78,27 @@ func TestLocalPreparer_ReuseRequiredValidatesRepositoryIdentity(t *testing.T) {
}
}
// @covers AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002.1
func TestValidateLocalRepositoryWorkspaceAcceptsCaseAliasedCommonDirectory(t *testing.T) {
parent := t.TempDir()
repositoryPath := filepath.Join(parent, "CaseIdentity")
if err := os.Mkdir(repositoryPath, 0o755); err != nil {
t.Fatalf("create mixed-case repository directory: %v", err)
}
initGitRepoAt(t, repositoryPath)
caseAlias := filepath.Join(parent, "caseidentity")
if _, err := os.Stat(caseAlias); err != nil {
if errors.Is(err, os.ErrNotExist) {
t.Skip("filesystem is case-sensitive")
}
t.Fatalf("inspect case-aliased repository path: %v", err)
}
if err := validateLocalRepositoryWorkspace(context.Background(), repositoryPath, caseAlias); err != nil {
t.Fatalf("validateLocalRepositoryWorkspace() rejected the same repository through a case alias: %v", err)
}
}
func TestLocalPreparer_RejectsRepoBackedNonGitWorkspace(t *testing.T) {
workspacePath := t.TempDir()
preparer := NewLocalPreparer(newTestLocalLogger())
@@ -116,6 +137,11 @@ func TestLocalPreparer_AcceptsMatchingRepoBackedGitWorkspace(t *testing.T) {
func initGitRepo(t *testing.T) string {
t.Helper()
dir := t.TempDir()
return initGitRepoAt(t, dir)
}
func initGitRepoAt(t *testing.T, dir string) string {
t.Helper()
// Start from a clean env: filter all GIT_* vars that may leak from parent
// processes (e.g. pre-commit hooks set GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE).
gitEnv := filterLocalTestGitEnv(os.Environ())