fix(worktree): collapse duplicate "Local Mac" run targets in the host picker (#10472)

* fix(worktree): collapse duplicate "Local Mac" run targets in the host picker

A linked worktree added as its own project projects a second ready host
setup on the same project+host, so the run-target picker rendered N
identical "Local Mac" rows differing only by path. Only the first was
reachable — resolveWorkspaceCreationTarget takes the first project+host
match — so the extras pointed at paths that may no longer exist.

- Dedupe ready setup options by host in the picker (display fix for
  profiles that already hold duplicates).
- Canonicalize a stale draft's setup id to the setup the picker shows,
  so the displayed path is the path the workspace is created in.
- Reject a linked worktree at repos:add when its main checkout is
  already tracked, preventing new duplicates.

* fix(worktree): only dedupe a linked worktree against a git main checkout

Review follow-up: the repos:add guard matched any tracked repo on the main
checkout path, including a folder-kind record. A folder repo does not
project onto the same project as the git worktree, so matching it would
suppress a legitimate add without deduping anything.
This commit is contained in:
Neil
2026-07-25 15:38:35 -07:00
committed by GitHub
parent 56d3e2cb2e
commit eb545aaa59
8 changed files with 457 additions and 4 deletions
+85 -1
View File
@@ -11,7 +11,12 @@ import {
import { tmpdir } from 'node:os'
import * as path from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { getGitRepoRoot, isGitRepo, normalizeGitRepoRootForInputPath } from './repo'
import {
getGitRepoRoot,
getLinkedWorktreeMainRepoRoot,
isGitRepo,
normalizeGitRepoRootForInputPath
} from './repo'
function git(cwd: string, args: string[]): string {
return execFileSync('git', args, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] })
@@ -329,6 +334,85 @@ describe('isGitRepo', () => {
})
})
describe('getLinkedWorktreeMainRepoRoot', () => {
let tmpDir: string
beforeEach(() => {
tmpDir = mkdtempSync(path.join(tmpdir(), 'orca-linked-worktree-'))
})
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true })
})
function initRepoWithCommit(repoRoot: string): void {
mkdirSync(repoRoot, { recursive: true })
git(repoRoot, ['init', '--quiet'])
git(repoRoot, ['config', 'user.email', 'test@orca.test'])
git(repoRoot, ['config', 'user.name', 'Orca Test'])
writeFileSync(path.join(repoRoot, 'README.md'), 'seed\n')
git(repoRoot, ['add', 'README.md'])
git(repoRoot, ['commit', '--quiet', '-m', 'seed'])
}
it('resolves a linked worktree back to its main checkout', () => {
const repoRoot = path.join(tmpDir, 'repo')
initRepoWithCommit(repoRoot)
const linked = path.join(tmpDir, 'linked')
git(repoRoot, ['worktree', 'add', '--quiet', '-b', 'feature', linked])
const expectedMainRoot = git(repoRoot, ['rev-parse', '--show-toplevel'])
.trim()
.replace(/\\/g, '/')
expect(getLinkedWorktreeMainRepoRoot(linked)).toBe(expectedMainRoot)
})
it('returns null for the main checkout itself', () => {
const repoRoot = path.join(tmpDir, 'repo')
initRepoWithCommit(repoRoot)
expect(getLinkedWorktreeMainRepoRoot(repoRoot)).toBeNull()
})
it('returns null for a nested directory inside the main checkout', () => {
const repoRoot = path.join(tmpDir, 'repo')
initRepoWithCommit(repoRoot)
const nested = path.join(repoRoot, 'packages', 'web')
mkdirSync(nested, { recursive: true })
expect(getLinkedWorktreeMainRepoRoot(nested)).toBeNull()
})
it('returns null for a bare repository', () => {
const bareRepo = path.join(tmpDir, 'bare.git')
git(tmpDir, ['init', '--bare', '--quiet', bareRepo])
expect(getLinkedWorktreeMainRepoRoot(bareRepo)).toBeNull()
})
it('returns null for a non-repository directory', () => {
const plain = path.join(tmpDir, 'plain')
mkdirSync(plain)
expect(getLinkedWorktreeMainRepoRoot(plain)).toBeNull()
})
it('returns null for a missing path', () => {
expect(getLinkedWorktreeMainRepoRoot(path.join(tmpDir, 'does-not-exist'))).toBeNull()
})
it('returns null when git cannot be run rather than guessing a main checkout', () => {
const repoRoot = path.join(tmpDir, 'repo')
initRepoWithCommit(repoRoot)
const linked = path.join(tmpDir, 'linked')
git(repoRoot, ['worktree', 'add', '--quiet', '-b', 'feature', linked])
withGitUnavailable(() => {
expect(getLinkedWorktreeMainRepoRoot(linked)).toBeNull()
})
})
})
/**
* Run `fn` with `git` removed from PATH so the in-process git probe fails the
* same way a transient spawn failure would, exercising the `.git`-marker
+46
View File
@@ -155,6 +155,52 @@ export function getGitRepoRoot(path: string): string {
return path
}
function canonicalizeGitDirPath(path: string): string {
return resolveRealPathSync(path) ?? path
}
/**
* Main-checkout path when `path` is a *linked* worktree, else null (main worktree, bare repo,
* non-repo, or any git failure). A linked worktree's `--git-dir` is `<common>/worktrees/<name>`
* while the main worktree's equals `--git-common-dir`; comparing the two from one invocation is
* git's own canonical test and avoids symlink-canonicalization mismatches. Baseline-safe: both
* flags long predate Git 2.25, and a relative answer resolves against `path` as old Git reports it.
*/
export function getLinkedWorktreeMainRepoRoot(path: string): string | null {
try {
if (!existsSync(path) || !statSync(path).isDirectory()) {
return null
}
if (gitExecFileSync(['rev-parse', '--is-inside-work-tree'], { cwd: path }).trim() !== 'true') {
return null
}
const [gitDir, commonDir] = gitExecFileSync(['rev-parse', '--git-dir', '--git-common-dir'], {
cwd: path
})
.split('\n')
.map((line) => line.trim())
if (!gitDir || !commonDir) {
return null
}
// Why realpath both: git answers one flag absolutely (already symlink-resolved) and the other
// relative to cwd, so a repo under a symlinked root (macOS /var -> /private/var) compares
// unequal on raw strings and a main checkout gets misread as a linked worktree.
const absoluteCommonDir = canonicalizeGitDirPath(resolve(path, commonDir))
if (canonicalizeGitDirPath(resolve(path, gitDir)) === absoluteCommonDir) {
return null
}
// A bare/separate git dir has no adjacent working checkout to point at.
if (basename(absoluteCommonDir) !== '.git') {
return null
}
// Re-resolve through getGitRepoRoot so the returned path matches the canonical form
// add-project stores for the main checkout (symlinks resolved the way git reports them).
return getGitRepoRoot(dirname(absoluteCommonDir))
} catch {
return null
}
}
export function normalizeGitRepoRootForInputPath(inputPath: string, rootPath: string): string {
const inputWsl = parseWslUncPath(inputPath)
if (inputWsl && rootPath.startsWith('/')) {