Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
ed60f5c
fix(worktrees): delete removed checkouts in git, not in Orca's file pool
brennanb2025 Sep 29, 2026
7e5acff
fix(worktrees): let Git delete Windows checkouts with long paths enabled
brennanb2025 Sep 29, 2026
97b3778
fix(worktrees): keep an inherited GIT_ASK_YESNO out of the worktree d…
brennanb2025 Sep 29, 2026
09d6707
perf(worktrees): run worktree deletes under their own limit, outside …
brennanb2025 Sep 29, 2026
3d92074
fix(worktrees): delete removed checkouts in the background and mark t…
brennanb2025 Sep 29, 2026
f4ef5bc
fix(worktrees): keep the existing Deleting card until the host's Git …
brennanb2025 Sep 29, 2026
5f5c3c9
test(worktrees): type the removal harness and projection for the node…
brennanb2025 Sep 29, 2026
5a2c9bd
Merge remote-tracking branch 'origin/main' into brennanb2025/worktree…
brennanb2025 Sep 29, 2026
fc75c61
fix(worktrees): don't fail a delete retry with an earlier attempt's b…
brennanb2025 Sep 29, 2026
c462aba
fix(worktrees): let only a gap in host events settle a background del…
brennanb2025 Sep 29, 2026
2992bd8
perf(worktrees): let a bulk delete start each same-repo checkout dele…
brennanb2025 Sep 29, 2026
0c6765d
test(worktrees): pin the host platform in the mocked removal suites s…
brennanb2025 Sep 29, 2026
aeb5c94
test(worktrees): type the blocked git remove result instead of a broa…
brennanb2025 Sep 29, 2026
e72f56c
test(worktrees): clear the changed-code quality gate in the removal s…
brennanb2025 Sep 29, 2026
5e14dc2
fix(worktrees): record each background delete durably and finish it a…
brennanb2025 Sep 29, 2026
2d71eb9
test(worktrees): type the interrupted-removal assertions for the node…
brennanb2025 Sep 29, 2026
09f75c4
fix(worktrees): finish an interrupted delete that already removed the…
brennanb2025 Sep 29, 2026
e6fa1a9
fix(worktrees): let Git finish an interrupted delete, and never take …
brennanb2025 Sep 29, 2026
ab2c4b3
test(worktrees): compare worktree paths by value and tear down with W…
brennanb2025 Sep 29, 2026
5534e95
fix(worktrees): reply to a worktree delete when it has finished, not …
brennanb2025 Sep 29, 2026
c13bece
test(worktrees): type the pending-removal host id in the background-r…
brennanb2025 Sep 29, 2026
aafc7eb
fix(worktrees): answer a delete request even when a concurrent remova…
brennanb2025 Sep 29, 2026
676ac07
Merge remote-tracking branch 'origin/main' into brennanb2025/worktree…
brennanb2025 Sep 29, 2026
032e520
fix(worktrees): run same-repo removal archive hooks and teardown one …
brennanb2025 Sep 29, 2026
52cdae9
fix(runtime): keep waiting worktree deletes out of a host's foregroun…
brennanb2025 Sep 29, 2026
c34bcc2
fix(worktrees): join a same-worktree delete accepted while a removal …
brennanb2025 Sep 29, 2026
8c81fca
fix(worktrees): fence a resumed delete's checkout from startup, and d…
brennanb2025 Sep 29, 2026
567cb50
test(worktrees): keep git's auto-maintenance out of the real-git remo…
brennanb2025 Sep 29, 2026
2755748
fix(worktrees): one archive-hook approval covers a same-repo bulk del…
brennanb2025 Sep 29, 2026
177b7a1
Merge remote-tracking branch 'origin/main' into brennanb2025/worktree…
brennanb2025 Sep 29, 2026
ef3bc4f
fix(worktrees): a delete Git fails partway stays listed with its erro…
brennanb2025 Sep 29, 2026
13034a5
fix(renderer): show a failed delete's host error on its card
brennanb2025 Sep 30, 2026
89f9d88
test(worktrees): type the failed-removal listing and refresh mocks
brennanb2025 Sep 30, 2026
d46623b
fix(worktrees): a failed delete's retry never removes a checkout Git …
brennanb2025 Sep 30, 2026
361b929
fix(worktrees): a failed delete's record ends at startup once its rep…
brennanb2025 Sep 30, 2026
b0f5472
fix(renderer): a failed delete's row offers Remove from Orca
brennanb2025 Sep 30, 2026
46bfbbf
test(worktrees): type the failed-removal IPC test's module mocks with…
brennanb2025 Sep 30, 2026
1b3ba5c
Merge remote-tracking branch 'origin/main' into brennanb2025/worktree…
brennanb2025 Sep 30, 2026
0ed46bf
fix(worktrees): Delete picks retry or a normal delete from Git's curr…
brennanb2025 Sep 30, 2026
a3b5f9d
fix(worktrees): a failed delete's retry checks the leftover again rig…
brennanb2025 Sep 30, 2026
4e91d3f
fix(worktrees): Remove from Orca reaches paired clients and matches t…
brennanb2025 Sep 30, 2026
4ca8c6e
Merge remote-tracking branch 'origin/brennanb2025/worktree-remove-in-…
brennanb2025 Sep 30, 2026
a4391d0
fix(worktrees): a failed delete's retry re-lists only its own repo's …
brennanb2025 Sep 30, 2026
633c4e5
fix(worktrees): a second Delete joins a retry already running, and th…
brennanb2025 Sep 30, 2026
e0b2d04
Merge remote-tracking branch 'origin/main' into brennanb2025/worktree…
brennanb2025 Sep 30, 2026
0ae473c
fix(renderer): a failed delete's card says it failed, and Delete keep…
brennanb2025 Sep 30, 2026
7315064
Merge remote-tracking branch 'origin/brennanb2025/worktree-remove-in-…
brennanb2025 Sep 30, 2026
e063629
fix(renderer): a failed delete's dialog shows the host's error, and i…
brennanb2025 Sep 30, 2026
d8db5a7
Merge remote-tracking branch 'origin/main' into brennanb2025/worktree…
brennanb2025 Sep 30, 2026
452ba2d
style(worktrees): import the removal result types in one statement
brennanb2025 Oct 1, 2026
754a446
test(worktrees): a runtime listing right after a failed delete shows …
brennanb2025 Oct 1, 2026
88e4848
fix(worktrees): pass the runtime retry's PTY-stop waiver in the shape…
brennanb2025 Oct 1, 2026
c18a2d1
Merge remote-tracking branch 'origin/main' into brennanb2025/worktree…
brennanb2025 Oct 1, 2026
49a8ec7
Merge remote-tracking branch 'origin/main' into brennanb2025/worktree…
brennanb2025 Oct 1, 2026
9b847b7
fix(worktrees): drop Remove from Orca from failed-delete rows
brennanb2025 Oct 2, 2026
47621c5
test(worktrees): wait for the dropped record's write before the faile…
brennanb2025 Oct 2, 2026
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
2 changes: 1 addition & 1 deletion src/main/git/worktree-add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type {
} from './worktree-operation-options'
import { gitExecOptions, resolveWorktreeAddTimeoutMs } from './worktree-operation-options'
import { bumpWorktreeScanGeneration } from './worktree-scan-cache'
import { assertNoPendingWorktreeRemovalConflict } from '../worktree-background-removal'
import { assertNoPendingWorktreeRemovalConflict } from '../worktree-removal-table'

export type WorktreeAddBaseContext = Pick<AddWorktreeResult, 'localBaseRefUpdateSuggestion'> & {
effectiveBase: string
Expand Down
8 changes: 7 additions & 1 deletion src/main/git/worktree-removal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,21 @@ function deleteBranchOfRemovedWorktree(
/**
* Finishes a removal whose checkout Git no longer registers (it finished deleting, or an earlier
* run did): leftover files, stale admin records, then the branch. Already-gone parts are done.
* `assertLeftover` refuses unless the path still holds the removed checkout's own leftover.
*/
export async function finishUnregisteredWorktreeRemoval(
repoPath: string,
worktreePath: string,
branch: { name: string; head: string } | null,
assertLeftover: () => Promise<void>,
options: RemoveWorktreeOptions = {}
): Promise<RemoveWorktreeResult> {
try {
await runUnderWorktreeDeleteLimit(() => removeCheckoutLeftByGit(worktreePath, options))
await runUnderWorktreeDeleteLimit(async () => {
// Why in the slot: the wait can outlast two large deletes, and the path may change meanwhile.
await assertLeftover()
await removeCheckoutLeftByGit(worktreePath, options)
})
await gitExecFileAsync(['worktree', 'prune'], gitExecOptions(repoPath, options)).catch(
(error: unknown) => console.warn(`[git] worktree prune failed in ${repoPath}`, error)
)
Expand Down
1 change: 1 addition & 0 deletions src/main/ipc/worktree-metadata-merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export function mergeWorktree(
isBare: git.isBare,
...(git.isSparse === true ? { isSparse: true } : {}),
isMainWorktree: git.isMainWorktree,
...(git.removalError ? { removalError: git.removalError } : {}),
// Automatic labels follow the live branch; persisted values are only authoritative when pinned.
displayName:
meta?.displayNameIsPinned === false
Expand Down
2 changes: 1 addition & 1 deletion src/main/ipc/worktree-remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ import {
import { createRetiredNameLookup } from '../../shared/worktree/retired-name-registry'
import { toLocalBaseRefRefreshResult } from '../../shared/worktree/local-base-branch-fast-forward'
import { isSshRequestOutcomeUnverifiable } from '../ssh/ssh-channel-multiplexer'
import { findPendingWorktreeRemovalConflict } from '../worktree-background-removal'
import { findPendingWorktreeRemovalConflict } from '../worktree-removal-table'

const SSH_WORKTREE_CREATE_FETCH_FRESHNESS_MS = 30_000
const SSH_WORKTREE_CREATE_FETCH_CACHE_MAX = 512
Expand Down
252 changes: 252 additions & 0 deletions src/main/ipc/worktrees-failed-removal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
// Desktop IPC for a delete that failed after Git dropped the registration: the leftover stays in
// `worktrees:list` with the error, Delete retries it. Git and the disk are mocked;
// runtime-failed-local-worktree-removal.test.ts runs the real thing.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
killAllProcessesForWorktreeMock,
listWorktreesMock,
removeWorktreeMock
} from './worktrees-test-module-mocks'
import { handlers, setupWorktreeHandlers, store } from './worktrees-test-harness'
import { mockKnownFeatureWorktree } from './worktrees-test-fixtures'
import type { RemoveWorktreeResult } from '../../shared/worktree/create-types'
import type { Worktree } from '../../shared/worktree/types'
import { finishUnregisteredWorktreeRemoval } from '../git/worktree-removal'
import type * as WorktreeRemovalModule from '../git/worktree-removal'
import type * as WorktreeRemovalTable from '../worktree-removal-table'
import type * as WorktreeRemovalLeftover from '../worktree-removal-leftover'
import {
_resetPendingWorktreeRemovalsForTests,
_settlePendingWorktreeRemovalsForTests,
retryFailedWorktreeRemoval,
startBackgroundWorktreeRemoval
} from '../worktree-background-removal'

vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
)
vi.mock('../git/worktree', async () =>
(await import('./worktrees-test-module-mocks')).gitWorktreeModuleMock()
)
vi.mock('../git/runner', async () =>
(await import('./worktrees-test-module-mocks')).gitRunnerModuleMock()
)
vi.mock('../git/repo', async () =>
(await import('./worktrees-test-module-mocks')).gitRepoModuleMock()
)
vi.mock('../git/git-username', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
resolveLocalGitUsername: (await import('./worktrees-test-module-mocks'))
.resolveLocalGitUsernameMock
}))
vi.mock('../github/client', async () =>
(await import('./worktrees-test-module-mocks')).githubClientModuleMock()
)
vi.mock('../source-control/hosted-review', async () =>
(await import('./worktrees-test-module-mocks')).hostedReviewModuleMock()
)
vi.mock('../providers/ssh-git-dispatch', async () =>
(await import('./worktrees-test-module-mocks')).sshGitDispatchModuleMock()
)
vi.mock('../providers/ssh-filesystem-dispatch', async () =>
(await import('./worktrees-test-module-mocks')).sshFilesystemDispatchModuleMock()
)
vi.mock('./worktree-symlinks', async () =>
(await import('./worktrees-test-module-mocks')).worktreeSymlinksModuleMock()
)
vi.mock('./ssh', async () => (await import('./worktrees-test-module-mocks')).sshModuleMock())
vi.mock('../ssh/ssh-target-registry', async () =>
(await import('./worktrees-test-module-mocks')).sshTargetRegistryModuleMock()
)
vi.mock('../hooks', async () => (await import('./worktrees-test-module-mocks')).hooksModuleMock())
vi.mock('../setup-runner-script-text', async (importOriginal) =>
(await import('./worktrees-test-module-mocks')).setupRunnerScriptTextModuleMock(
await importOriginal<Record<string, unknown>>()
)
)
vi.mock('../worktree-runner-script', async (importOriginal) =>
(await import('./worktrees-test-module-mocks')).worktreeRunnerScriptModuleMock(
await importOriginal<Record<string, unknown>>()
)
)
vi.mock('../effective-hook-config', async (importOriginal) =>
(await import('./worktrees-test-module-mocks')).effectiveHookConfigModuleMock(
await importOriginal<Record<string, unknown>>()
)
)
vi.mock('../setup-hook-env-vars', async (importOriginal) =>
(await import('./worktrees-test-module-mocks')).setupHookEnvVarsModuleMock(
await importOriginal<Record<string, unknown>>()
)
)
vi.mock('./worktree-logic', async (importOriginal) =>
(await import('./worktrees-test-module-mocks')).worktreeLogicModuleMock(
await importOriginal<Record<string, unknown>>()
)
)
vi.mock('../terminal-history-deletion', async () =>
(await import('./worktrees-test-module-mocks')).terminalHistoryDeletionModuleMock()
)
vi.mock('../ports/advertised-url-watcher', async () =>
(await import('./worktrees-test-module-mocks')).advertisedUrlWatcherModuleMock()
)
vi.mock('../workspace-cleanup-scan-snapshot', async () =>
(await import('./worktrees-test-module-mocks')).workspaceCleanupScanSnapshotModuleMock()
)
vi.mock('../workspace-space-analysis-snapshot', async () =>
(await import('./worktrees-test-module-mocks')).workspaceSpaceAnalysisSnapshotModuleMock()
)
vi.mock('../workspace-cleanup-removal-snapshot-prune', async () =>
(await import('./worktrees-test-module-mocks')).workspaceCleanupRemovalSnapshotPruneModuleMock()
)
vi.mock('../runtime/worktree-teardown', async () =>
(await import('./worktrees-test-module-mocks')).worktreeTeardownModuleMock()
)
vi.mock('./pty', async () => (await import('./worktrees-test-module-mocks')).ptyModuleMock())

vi.mock('../git/worktree-removal', async (importOriginal) => ({
...(await importOriginal<typeof WorktreeRemovalModule>()),
finishUnregisteredWorktreeRemoval: vi.fn(async () => ({}))
}))
// The leftover is on disk and is the removed checkout's own (no `.git` left).
vi.mock('../worktree-removal-table', async (importOriginal) => ({
...(await importOriginal<typeof WorktreeRemovalTable>()),
worktreeCheckoutExists: vi.fn(async () => true)
}))
vi.mock('../worktree-removal-leftover', async (importOriginal) => ({
...(await importOriginal<typeof WorktreeRemovalLeftover>()),
isUnregisteredRemovalLeftover: vi.fn(async () => true)
}))

const featureId = 'repo-1::/workspace/feature-wt'
const GIT_ERROR = "error: failed to delete '/workspace/feature-wt': Operation not permitted"

function remove(args: Record<string, unknown>): Promise<RemoveWorktreeResult> {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: worktrees:remove resolves a RemoveWorktreeResult.
return handlers['worktrees:remove'](null, args) as Promise<RemoveWorktreeResult>
}

async function listFeature(): Promise<Worktree | undefined> {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: worktrees:list resolves the repo's Worktree rows.
const rows = (await handlers['worktrees:list'](null, { repoId: 'repo-1' })) as Worktree[]
return rows.find((row) => row.id === featureId)
}

/** Git fails partway and drops the registration, as `git worktree remove --force` does. */
async function failAfterGitDroppedIt(): Promise<void> {
const [main, feature] = mockKnownFeatureWorktree()
const result = startBackgroundWorktreeRemoval({
removal: {
worktreeId: featureId,
repoId: 'repo-1',
repoPath: '/workspace/repo',
worktree: feature,
deleteBranch: true,
force: false
},
run: async () => {
listWorktreesMock.mockResolvedValue([main])
throw new Error(GIT_ERROR)
},
publish: () => {}
})
await expect(result).rejects.toThrow(GIT_ERROR)
await _settlePendingWorktreeRemovalsForTests()
}

describe('a failed delete Git no longer registers, over desktop IPC', () => {
beforeEach(() => {
vi.spyOn(console, 'warn').mockImplementation(() => {})
setupWorktreeHandlers()
})

afterEach(() => {
_resetPendingWorktreeRemovalsForTests()
vi.mocked(finishUnregisteredWorktreeRemoval).mockClear()
})

it('stays in the listing with the error instead of vanishing', async () => {
await failAfterGitDroppedIt()

const row = await listFeature()
expect(row).toMatchObject({ path: '/workspace/feature-wt', removalError: GIT_ERROR })
expect(row?.removing).toBeUndefined()
})

it('Delete runs the recorded removal again: teardown, leftover, branch and metadata', async () => {
await failAfterGitDroppedIt()
killAllProcessesForWorktreeMock.mockClear()

await expect(remove({ worktreeId: featureId })).resolves.not.toHaveProperty('removing')

// Git has no registration to delete by; the leftover goes through Orca's own delete.
expect(removeWorktreeMock).not.toHaveBeenCalled()
expect(finishUnregisteredWorktreeRemoval).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/feature-wt',
{ name: 'feature', head: 'feature' },
expect.any(Function),
{}
)
expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith(
featureId,
expect.objectContaining({ requirePhysicalStop: true })
)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(featureId, 'local')
expect(await listFeature()).toBeUndefined()
})

it('keeps the row with the new error when the retry fails again', async () => {
await failAfterGitDroppedIt()
vi.mocked(finishUnregisteredWorktreeRemoval).mockRejectedValueOnce(new Error('EPERM again'))

await expect(remove({ worktreeId: featureId })).rejects.toThrow('EPERM again')
await _settlePendingWorktreeRemovalsForTests()

expect(await listFeature()).toMatchObject({ removalError: 'EPERM again' })
expect(store.removeWorktreeMeta).not.toHaveBeenCalled()
})

it('joins a retry another client started while this Delete listed Git', async () => {
await failAfterGitDroppedIt()
const [main] = mockKnownFeatureWorktree()
listWorktreesMock.mockResolvedValue([main])
const otherClientsRetry = vi.fn(async () => ({}))
listWorktreesMock.mockImplementationOnce(async () => {
// Another client's Delete takes the failed record during this Delete's `git worktree list`.
void retryFailedWorktreeRemoval(featureId, 'local', () => ({
run: otherClientsRetry,
publish: () => {}
}))
return [main]
})

await expect(remove({ worktreeId: featureId })).resolves.not.toHaveProperty('removing')
await _settlePendingWorktreeRemovalsForTests()

expect(otherClientsRetry).toHaveBeenCalledTimes(1)
// Neither a second retry nor the delete for leftovers without a record ran.
expect(finishUnregisteredWorktreeRemoval).not.toHaveBeenCalled()
expect(removeWorktreeMock).not.toHaveBeenCalled()
})

it('Delete takes the normal delete once Git registers a checkout at the path again', async () => {
await failAfterGitDroppedIt()
// A new checkout at the same path: the recorded choices were for the leftover, not for it.
mockKnownFeatureWorktree()
removeWorktreeMock.mockResolvedValue({})

await remove({ worktreeId: featureId, force: false })
await _settlePendingWorktreeRemovalsForTests()

expect(finishUnregisteredWorktreeRemoval).not.toHaveBeenCalled()
expect(removeWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/feature-wt',
false,
expect.anything()
)
listWorktreesMock.mockResolvedValue([mockKnownFeatureWorktree()[0]])
expect(await listFeature()).toBeUndefined()
})
})
10 changes: 7 additions & 3 deletions src/main/ipc/worktrees/listing/detected-provider-listing.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {
projectPendingWorktreeRemovals,
snapshotPendingWorktreeRemovals
} from '../../../worktree-background-removal'
snapshotPendingWorktreeRemovals,
withUnregisteredRemovalCheckouts
} from '../../../worktree-removal-listing'
import {
getRepoExecutionHostId,
getSshTargetIdForExecutionHost
Expand Down Expand Up @@ -149,6 +150,9 @@ export async function listDetectedWorktreesForCapturedRepo(
return abortedResult() ?? null
}
const { gitWorktrees, fresh: freshScan, sideEffectToken, metadataPrune, hygieneDue } = scan
const localRows = connectionId
? gitWorktrees
: await withUnregisteredRemovalCheckouts(repo.id, gitWorktrees)
const aborted = abortedResult()
if (aborted) {
return aborted
Expand Down Expand Up @@ -193,7 +197,7 @@ export async function listDetectedWorktreesForCapturedRepo(
? buildDetectedGitWorktrees(store, repo, gitWorktrees, allMeta)
: // Why always marked: the desktop renderer ships with this main process.
projectPendingWorktreeRemovals(
buildDetectedGitWorktrees(store, repo, gitWorktrees, allMeta),
buildDetectedGitWorktrees(store, repo, localRows, allMeta),
(worktree) => worktree.id,
true,
pendingAtScan
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ import type { Worktree } from '../../../../shared/worktree/types'
import {
projectPendingWorktreeRemovals,
snapshotPendingWorktreeRemovals,
withUnregisteredRemovalCheckouts,
type PendingWorktreeRemovals
} from '../../../worktree-background-removal'
} from '../../../worktree-removal-listing'
import { getLocalWorktreeScanGeneration } from '../../../local-worktree-scan-generation'
import { getRegisteredWorktreeRootsRevision } from '../../registered-worktree-roots-cache'

Expand Down Expand Up @@ -167,7 +168,10 @@ export function registerWorktreeCatalogHandlers(context: WorktreeIpcContext): vo
}
loggedWorktreeListFailures.delete(`${repo.id}:${repo.path}`)
const metadata = metadataForRepo(repo)
const worktrees = buildDetectedGitWorktrees(store, repo, gitWorktrees, metadata)
const rows = connectionId
? gitWorktrees
: await withUnregisteredRemovalCheckouts(repo.id, gitWorktrees)
const worktrees = buildDetectedGitWorktrees(store, repo, rows, metadata)
.filter((worktree) => worktree.visible)
.map((worktree) => stampAndMergeVisibleDetectedWorktree(store, repo, worktree, metadata))
return connectionId ? worktrees : markLocalWorktreesUnderRemoval(worktrees, pendingAtScan)
Expand Down Expand Up @@ -261,7 +265,10 @@ export function registerWorktreeCatalogHandlers(context: WorktreeIpcContext): vo
}
loggedWorktreeListFailures.delete(`${repo.id}:${repo.path}`)
const metadata = allMeta ?? readAllWorktreeMetaForRepo(store, repo)
const worktrees = buildDetectedGitWorktrees(store, repo, gitWorktrees, metadata)
const rows = connectionId
? gitWorktrees
: await withUnregisteredRemovalCheckouts(repo.id, gitWorktrees)
const worktrees = buildDetectedGitWorktrees(store, repo, rows, metadata)
.filter((worktree) => worktree.visible)
.map((worktree) => stampAndMergeVisibleDetectedWorktree(store, repo, worktree, metadata))
return connectionId ? worktrees : markLocalWorktreesUnderRemoval(worktrees, pendingAtScan)
Expand Down
10 changes: 10 additions & 0 deletions src/main/ipc/worktrees/removal/execute-worktree-removal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { removeFolderWorkspace } from './remove-folder-workspace'
import { removeUnregisteredWorktree } from './remove-unregistered-worktree'
import { removeRegisteredRemoteWorktree } from './remove-registered-remote-worktree'
import { removeRegisteredLocalWorktree } from './remove-registered-local-worktree'
import { retryFailedLocalWorktreeRemoval } from './retry-failed-local-worktree-removal'
import { retryFailedRemovalUnlessRegistered } from '../../../worktree-removal-table'

/**
* Refuses a repo row whose two host spellings disagree.
Expand Down Expand Up @@ -114,6 +116,14 @@ export async function executeWorktreeRemoval(
registeredWorktrees,
resolveWorktreeRemovalHomeForHost(removalHostId)
)
if (
!repo.connectionId &&
retryFailedRemovalUnlessRegistered(args.worktreeId, worktreePath, registeredWorktrees, () =>
retryFailedLocalWorktreeRemoval(context, args, removalHostId)
)
) {
return { removing: true }
}
if (!registeredWorktree) {
return removeUnregisteredWorktree(
context,
Expand Down
Loading
Loading