fix(worktree): enforce retired names at creation

This commit is contained in:
Brennan Benson
2026-08-13 15:56:03 -07:00
parent b15c7e084b
commit 3b082a9470
5 changed files with 43 additions and 15 deletions
+13
View File
@@ -135,6 +135,7 @@ import {
getWorktreeCreateCandidate,
WORKTREE_CREATE_MAX_SUFFIX_ATTEMPTS
} from '../worktree-create-candidates'
import { getRetiredWorktreeNamesForRepo } from '../worktree-name-retirement'
const SSH_WORKTREE_CREATE_FETCH_FRESHNESS_MS = 30_000
const SSH_WORKTREE_CREATE_FETCH_CACHE_MAX = 512
@@ -1550,12 +1551,18 @@ export async function createRemoteWorktree(
let selectedExistingLocalBranchName: string | null = null
let lastBranchConflictKind: 'local' | 'remote' | null = null
let remotePathResolved = false
const retiredNames = new Set(
getRetiredWorktreeNamesForRepo(store, repo, store.getRepos(), settings)
)
// Why: duplicate PR/MR checkouts still need a workspace; suffix branch/path while preserving review metadata and push target.
for (let suffix = 1; suffix <= WORKTREE_CREATE_MAX_SUFFIX_ATTEMPTS; suffix += 1) {
effectiveSanitizedName = getWorktreeCreateCandidate(sanitizedName, suffix)
effectiveRequestedName = args.name.trim()
? getWorktreeCreateCandidate(args.name, suffix)
: effectiveSanitizedName
if (retiredNames.has(effectiveSanitizedName)) {
continue
}
branchName = await resolveCreateBranchNameSsh(
provider,
repo.path,
@@ -2091,12 +2098,18 @@ export async function createLocalWorktree(
let lastBranchConflictKind: 'local' | 'remote' | null = null
let lastExistingPR: Awaited<ReturnType<typeof getPRForBranch>> | null = null
let lastExistingReviewNumber: number | null = null
const retiredNames = new Set(
getRetiredWorktreeNamesForRepo(store, repo, store.getRepos(), settings)
)
// Why: a create-from-review branch override may already exist locally; suffix both branch and path instead of blocking the user.
for (let suffix = 1; suffix <= WORKTREE_CREATE_MAX_SUFFIX_ATTEMPTS; suffix += 1) {
effectiveSanitizedName = getWorktreeCreateCandidate(sanitizedName, suffix)
effectiveRequestedName = requestedName.trim()
? getWorktreeCreateCandidate(requestedName, suffix)
: effectiveSanitizedName
if (retiredNames.has(effectiveSanitizedName)) {
continue
}
lastExistingReviewNumber = null
branchName = await resolveCreateBranchName(
+7 -6
View File
@@ -325,14 +325,15 @@ describe('registerWorktreeHandlers – Windows path handling', () => {
})
})
it('retires the generated name when flat layout decorates the physical leaf', async () => {
computeWorktreePathMock.mockReturnValue('C:\\workspaces\\repo-nautilus')
ensurePathWithinWorkspaceMock.mockReturnValue('C:\\workspaces\\repo-nautilus')
it('skips a retired generated name when the physical leaf is decorated', async () => {
store.getRetiredWorktreeNames.mockReturnValue(['nautilus'])
computeWorktreePathMock.mockReturnValue('C:\\workspaces\\repo-nautilus-2')
ensurePathWithinWorkspaceMock.mockReturnValue('C:\\workspaces\\repo-nautilus-2')
listWorktreesMock.mockResolvedValue([
{
path: 'C:/workspaces/repo-nautilus',
path: 'C:/workspaces/repo-nautilus-2',
head: 'abc123',
branch: 'refs/heads/nautilus',
branch: 'refs/heads/nautilus-2',
isBare: false,
isMainWorktree: false
}
@@ -340,7 +341,7 @@ describe('registerWorktreeHandlers – Windows path handling', () => {
await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'nautilus' })
expect(store.addRetiredWorktreeName).toHaveBeenCalledWith('repo-1', 'nautilus')
expect(store.addRetiredWorktreeName).toHaveBeenCalledWith('repo-1', 'nautilus-2')
})
it('resolves the Git username when the configured prefix consumes it', async () => {
+7 -4
View File
@@ -4672,9 +4672,9 @@ describe('registerWorktreeHandlers', () => {
isMainWorktree: true
},
{
path: '/remote/repo-nautilus',
path: '/remote/repo-nautilus-2',
head: 'abc123',
branch: 'refs/heads/nautilus',
branch: 'refs/heads/nautilus-2',
isBare: false,
isMainWorktree: false
}
@@ -4689,6 +4689,9 @@ describe('registerWorktreeHandlers', () => {
store.getRepo.mockReturnValue(repo)
getSshGitProviderMock.mockReturnValue(provider)
getActiveMultiplexerMock.mockReturnValue(mux)
store.getRetiredWorktreeNames.mockImplementation((repoId) =>
repoId === 'repo-ssh' ? ['nautilus'] : []
)
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
const result = await handlers['worktrees:create'](null, {
@@ -4711,9 +4714,9 @@ describe('registerWorktreeHandlers', () => {
)
expect(provider.listWorktrees).toHaveBeenCalledTimes(1)
expect(provider.worktreeIsClean).not.toHaveBeenCalled()
expect(store.addRetiredWorktreeName).toHaveBeenCalledWith('repo-ssh', 'nautilus')
expect(store.addRetiredWorktreeName).toHaveBeenCalledWith('repo-ssh', 'nautilus-2')
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-ssh::/remote/repo-nautilus',
'repo-ssh::/remote/repo-nautilus-2',
expect.objectContaining({
linkedIssue: 123,
linkedPR: 456,
+8 -4
View File
@@ -4052,12 +4052,16 @@ describe('OrcaRuntimeService', () => {
it('does not reuse stale in-flight worktree scans after creating a worktree', async () => {
const addRetiredWorktreeName = vi.fn()
const runtime = new OrcaRuntimeService({ ...store, addRetiredWorktreeName })
const runtime = new OrcaRuntimeService({
...store,
addRetiredWorktreeName,
getRetiredWorktreeNames: () => ['nautilus']
})
const staleScan = deferred<typeof MOCK_GIT_WORKTREES>()
const createdWorktree = {
path: '/tmp/workspaces/repo-nautilus',
path: '/tmp/workspaces/repo-nautilus-2',
head: 'def',
branch: 'nautilus',
branch: 'nautilus-2',
isBare: false,
isMainWorktree: false
}
@@ -4087,7 +4091,7 @@ describe('OrcaRuntimeService', () => {
worktrees: expect.arrayContaining([expect.objectContaining({ path: createdWorktree.path })])
}
)
expect(addRetiredWorktreeName).toHaveBeenCalledWith(TEST_REPO_ID, 'nautilus')
expect(addRetiredWorktreeName).toHaveBeenCalledWith(TEST_REPO_ID, 'nautilus-2')
})
it('creates additional workspace metadata for folder-mode repos through runtime create', async () => {
+8 -1
View File
@@ -935,7 +935,8 @@ import {
} from '../worktree-create-candidates'
import {
ensureRetiredWorktreeNamesBackfilled,
getRetiredWorktreeNamesByRepo
getRetiredWorktreeNamesByRepo,
getRetiredWorktreeNamesForRepo
} from '../worktree-name-retirement'
import { normalizeSparseDirectories } from '../ipc/sparse-checkout-directories'
import type { PtyBindingSourceExpectation, Store } from '../persistence'
@@ -22297,6 +22298,9 @@ export class OrcaRuntimeService {
let branchConflictKind: 'local' | 'remote' | null = null
let worktreePath = ''
let worktreePathResolved = false
const retiredNames = new Set(
getRetiredWorktreeNamesForRepo(this.store, repo, this.store.getRepos(), settings)
)
// Why: runtime/mobile create-from-review callers should get a new workspace
// even when the PR branch or review branch name is already in use.
for (let suffix = 1; suffix <= WORKTREE_CREATE_MAX_SUFFIX_ATTEMPTS; suffix += 1) {
@@ -22304,6 +22308,9 @@ export class OrcaRuntimeService {
effectiveRequestedName = args.name.trim()
? getWorktreeCreateCandidate(args.name, suffix)
: effectiveSanitizedName
if (retiredNames.has(effectiveSanitizedName)) {
continue
}
branchName = await resolveCreateBranchName(
repo.path,
selectedExistingLocalBranchName ??