Show Git-created worktrees in external discovery (#7078)

This commit is contained in:
Brennan Benson 2026-07-02 11:24:07 -07:00 committed by GitHub
parent 1ac6de1ccb
commit 4c03924618
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 209 additions and 100 deletions

View File

@ -1402,13 +1402,9 @@ export function registerWorktreeHandlers(
if (!registeredWorktree) {
const fsProvider = repo.connectionId ? getSshFilesystemProvider(repo.connectionId) : null
let canCleanOrphanedDirectory = false
const knownOrcaLayouts = buildKnownOrcaWorkspaceLayouts(store.getSettings(), repo)
if (
canCleanupUnregisteredOrcaWorktreeDirectory({
meta: removedMeta,
worktreePath,
repo,
knownOrcaLayouts
meta: removedMeta
})
) {
if (repo.connectionId) {
@ -1481,7 +1477,6 @@ export function registerWorktreeHandlers(
runtimeWorktreePath,
repo,
runtimeRepoPath: toLocalWorktreeRuntimePath(repo.path, localWorktreeGitOptions),
knownOrcaLayouts,
registeredWorktrees,
statPath: access.statPath,
isGitRepository: (path) => isLocalGitRepository(path, localWorktreeGitOptions)

View File

@ -14713,13 +14713,9 @@ export class OrcaRuntimeService {
)
if (!registeredWorktree) {
let canCleanOrphanedDirectory = false
const knownOrcaLayouts = buildKnownOrcaWorkspaceLayouts(store.getSettings(), repo)
if (
canCleanupUnregisteredOrcaWorktreeDirectory({
meta: removedMeta,
worktreePath: removalTarget.path,
repo,
knownOrcaLayouts
meta: removedMeta
})
) {
if (repo.connectionId) {
@ -14792,7 +14788,6 @@ export class OrcaRuntimeService {
runtimeWorktreePath,
repo,
runtimeRepoPath: toLocalWorktreeRuntimePath(repo.path, localWorktreeGitOptions),
knownOrcaLayouts,
registeredWorktrees,
statPath: access.statPath,
isGitRepository: (path) => isLocalRuntimeGitRepository(path, localWorktreeGitOptions)

View File

@ -4,6 +4,7 @@ import {
isWorktreePathMissing,
stripOrcaProvenanceMetaUpdates
} from './worktree-removal-safety'
import type { WorktreeMeta } from '../shared/types'
describe('isWorktreePathMissing', () => {
it('recognizes missing-path errors from local and remote stat providers', async () => {
@ -33,10 +34,7 @@ describe('canCleanupUnregisteredOrcaWorktreeDirectory', () => {
it('does not treat orcaCreatedAt alone as cleanup authority', () => {
expect(
canCleanupUnregisteredOrcaWorktreeDirectory({
meta: { orcaCreatedAt: Date.now() },
worktreePath: '/outside/orphan',
repo: { path: '/repo' },
knownOrcaLayouts: []
meta: { orcaCreatedAt: Date.now() }
})
).toBe(false)
expect(
@ -44,10 +42,7 @@ describe('canCleanupUnregisteredOrcaWorktreeDirectory', () => {
meta: {
orcaCreatedAt: Date.now(),
orcaCreationSource: 'runtime'
},
worktreePath: '/outside/orphan',
repo: { path: '/repo' },
knownOrcaLayouts: []
}
})
).toBe(true)
})
@ -55,32 +50,40 @@ describe('canCleanupUnregisteredOrcaWorktreeDirectory', () => {
it('accepts legacy Orca-created metadata before explicit provenance existed', () => {
expect(
canCleanupUnregisteredOrcaWorktreeDirectory({
meta: { createdAt: Date.now() },
worktreePath: '/outside/orphan',
repo: { path: '/repo' },
knownOrcaLayouts: []
meta: { createdAt: Date.now() }
})
).toBe(true)
})
it('accepts legacy repo-nested Orca workspace paths without metadata provenance', () => {
it('does not treat creation layout metadata alone as cleanup authority', () => {
const layoutOnlyMeta: WorktreeMeta = {
displayName: '',
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
linkedGitLabMR: null,
linkedGitLabIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
workspaceStatus: 'todo',
orcaCreationWorkspaceLayout: { path: '/orca/workspaces', nestWorkspaces: true }
}
expect(
canCleanupUnregisteredOrcaWorktreeDirectory({
meta: undefined,
worktreePath: '/orca/workspaces/app/legacy-orphan',
repo: { path: '/repos/app' },
knownOrcaLayouts: [{ path: '/orca/workspaces', nestWorkspaces: true }]
meta: layoutOnlyMeta
})
).toBe(true)
).toBe(false)
})
it('does not trust flat workspace-root paths without legacy metadata', () => {
it('does not trust paths without provenance or legacy metadata', () => {
expect(
canCleanupUnregisteredOrcaWorktreeDirectory({
meta: undefined,
worktreePath: '/orca/workspaces/legacy-orphan',
repo: { path: '/repos/app' },
knownOrcaLayouts: [{ path: '/orca/workspaces', nestWorkspaces: false }]
meta: undefined
})
).toBe(false)
})

View File

@ -325,7 +325,6 @@ describe('canCleanupUnregisteredOrcaLeftoverDirectory', () => {
runtimeWorktreePath: '/workspaces/orca-owned',
repo,
runtimeRepoPath: repo.path,
knownOrcaLayouts: [],
registeredWorktrees: [makeGitWorktree(repo.path, true)]
}
@ -376,7 +375,6 @@ describe('canCleanupUnregisteredOrcaLeftoverDirectory', () => {
canCleanupUnregisteredOrcaLeftoverDirectory({
...baseArgs,
meta: undefined,
knownOrcaLayouts: [{ path: '/workspaces', nestWorkspaces: false }],
statPath: makeStatPath([], ['/workspaces/orca-owned']),
isGitRepository
})

View File

@ -2,8 +2,7 @@ import { lstat, readFile } from 'node:fs/promises'
import { homedir } from 'node:os'
import { posix, win32 } from 'node:path'
import { isWindowsAbsolutePathLike } from '../shared/cross-platform-path'
import type { GitWorktreeInfo, OrcaWorkspaceLayout, Repo, WorktreeMeta } from '../shared/types'
import { matchesStrongOrcaCreatePath } from '../shared/worktree-ownership'
import type { GitWorktreeInfo, Repo, WorktreeMeta } from '../shared/types'
import { areWorktreePathsEqual } from './ipc/worktree-logic'
import {
gitFileProvesOrphanedWorktreeDirectory,
@ -172,9 +171,6 @@ export async function canSafelyRemoveOrphanedWorktreeDirectory(
export function canCleanupUnregisteredOrcaWorktreeDirectory(args: {
meta: UnregisteredOrcaCleanupMeta | null | undefined
worktreePath: string
repo: Pick<Repo, 'path'>
knownOrcaLayouts: readonly OrcaWorkspaceLayout[]
}): boolean {
if (hasCurrentOrcaCreationProvenance(args.meta)) {
return true
@ -184,9 +180,9 @@ export function canCleanupUnregisteredOrcaWorktreeDirectory(args: {
return true
}
// Why: profiles created before explicit provenance can still contain Orca
// workspaces at the repo-specific workspaceDir/<repo>/<name> path shape.
return matchesStrongOrcaCreatePath(args.worktreePath, args.knownOrcaLayouts, args.repo)
// Why: path shape alone is not authority; users can create plain Git
// worktrees inside Orca's workspace directory too.
return false
}
export async function canCleanupUnregisteredOrcaLeftoverDirectory(args: {
@ -195,7 +191,6 @@ export async function canCleanupUnregisteredOrcaLeftoverDirectory(args: {
runtimeWorktreePath: string
repo: Pick<Repo, 'path'>
runtimeRepoPath: string
knownOrcaLayouts: readonly OrcaWorkspaceLayout[]
registeredWorktrees: readonly GitWorktreeInfo[]
statPath: StatPath
isGitRepository: (runtimeWorktreePath: string) => Promise<boolean>

View File

@ -1,13 +1,23 @@
import { describe, expect, it } from 'vitest'
import type { DetectedWorktree, DetectedWorktreeListResult, Repo } from './types'
import type {
DetectedWorktree,
DetectedWorktreeListResult,
GlobalSettings,
Repo,
Worktree
} from './types'
import {
getHiddenExternalWorktrees,
getNewExternalWorktreeInboxWorktrees,
mergeExternalWorktreeInboxPaths,
shouldOfferNewExternalWorktreeInbox
} from './external-worktree-inbox'
import { EXTERNAL_WORKTREE_VISIBILITY_ROLLOUT_AT } from './worktree-ownership'
import {
buildKnownOrcaWorkspaceLayouts,
EXTERNAL_WORKTREE_VISIBILITY_ROLLOUT_AT,
toDetectedWorktree
} from './worktree-ownership'
const repo: Repo = {
id: 'repo-1',
@ -54,6 +64,55 @@ function detectedResult(worktrees: DetectedWorktree[]): DetectedWorktreeListResu
}
}
function makeSettings(): GlobalSettings {
return {
workspaceDir: '/orca/workspaces',
nestWorkspaces: true,
workspaceDirHistory: [],
refreshLocalBaseRefOnWorktreeCreate: false,
localBaseRefSuggestionDismissed: false,
branchPrefix: 'none',
branchPrefixCustom: '',
enableGitHubAttribution: false,
theme: 'system',
appFontFamily: 'Geist',
editorAutoSave: false,
editorAutoSaveDelayMs: 1000,
editorMinimapEnabled: false,
markdownReviewToolsEnabled: true,
terminalFontSize: 14,
terminalFontFamily: 'monospace',
terminalFontWeight: 400,
terminalLineHeight: 1.2
} as unknown as GlobalSettings
}
function makeGitWorktree(overrides: Partial<Worktree> = {}): Worktree {
return {
id: `repo-1::${overrides.path ?? '/repo'}`,
repoId: repo.id,
path: '/repo',
displayName: 'repo',
branch: 'refs/heads/main',
head: 'abc123',
isBare: false,
isMainWorktree: true,
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
linkedGitLabMR: null,
linkedGitLabIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
workspaceStatus: 'todo',
...overrides
}
}
describe('external worktree inbox', () => {
it('merges inbox paths without duplicates when paths match after normalization', () => {
expect(mergeExternalWorktreeInboxPaths(['/repo/one/'], ['/repo/one', '/repo/two'])).toEqual([
@ -103,6 +162,25 @@ describe('external worktree inbox', () => {
).toEqual([hidden])
})
it('offers metadata-free nested Orca workspace worktrees through the inbox', () => {
const settings = makeSettings()
const manual = toDetectedWorktree({
repo,
settings,
worktree: makeGitWorktree({
path: '/orca/workspaces/orca/manual-from-git',
displayName: 'manual-from-git',
branch: 'refs/heads/manual-from-git',
isMainWorktree: false
}),
knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo)
})
expect(manual.ownership).toBe('external')
expect(manual.visible).toBe(false)
expect(getNewExternalWorktreeInboxWorktrees(detectedResult([manual]), repo)).toEqual([manual])
})
it('suppresses non-authoritative detected results', () => {
expect(
getNewExternalWorktreeInboxWorktrees(detectedResult([detectedWorktree()]), {

View File

@ -48,7 +48,7 @@ describe('repo-specific worktree ownership layouts', () => {
worktree: makeWorktree('/projects/a/worktrees/repo/feature'),
knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repoA)
})
).toBe('orca-managed')
).toBe('external')
expect(
classifyWorktreeOwnership({
repo: repoB,
@ -73,7 +73,7 @@ describe('repo-specific worktree ownership layouts', () => {
worktree: makeWorktree('C:\\projects\\App\\worktrees\\repo\\Feature'),
knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo)
})
).toBe('orca-managed')
).toBe('external')
})
it('includes relative global layouts for SSH repos without applying absolute desktop paths', () => {

View File

@ -108,7 +108,7 @@ describe('worktree ownership classification', () => {
).toBe('orca-managed')
})
it('requires the nested repo-specific path shape for path-only ownership', () => {
it('treats nested Orca workspace paths without metadata as external', () => {
const repo = makeRepo()
const settings = makeSettings()
const layouts = buildKnownOrcaWorkspaceLayouts(settings, repo)
@ -119,7 +119,7 @@ describe('worktree ownership classification', () => {
worktree: makeWorktree({ path: '/orca/workspaces/app/feature' }),
knownOrcaLayouts: layouts
})
).toBe('orca-managed')
).toBe('external')
expect(
classifyWorktreeOwnership({
repo,
@ -130,6 +130,94 @@ describe('worktree ownership classification', () => {
).toBe('external')
})
it('treats explicit Orca creation layout metadata as managed', () => {
const repo = makeRepo()
const settings = makeSettings()
expect(
classifyWorktreeOwnership({
repo,
settings,
worktree: makeWorktree({ path: '/orca/workspaces/app/feature' }),
meta: makeMeta({
orcaCreationWorkspaceLayout: { path: '/orca/workspaces', nestWorkspaces: true }
}),
knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo)
})
).toBe('orca-managed')
})
it('does not treat metadata-free nested workspace paths as Orca-managed for new repos', () => {
const repo = makeRepo({ externalWorktreeVisibility: 'hide' })
const settings = makeSettings()
const detected = toDetectedWorktree({
repo,
settings,
worktree: makeWorktree({
path: '/orca/workspaces/app/manual-git-worktree',
isMainWorktree: false
}),
knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo)
})
expect(detected.ownership).toBe('external')
expect(detected.visible).toBe(false)
})
it('does not treat generic discovery metadata on nested workspace paths as Orca-managed', () => {
const repo = makeRepo({ externalWorktreeVisibility: 'hide' })
const settings = makeSettings()
const detected = toDetectedWorktree({
repo,
settings,
worktree: makeWorktree({
path: '/orca/workspaces/app/manual-git-worktree',
isMainWorktree: false
}),
meta: makeMeta({ displayName: 'manual-git-worktree' }),
knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo)
})
expect(detected.ownership).toBe('external')
expect(detected.visible).toBe(false)
})
it('keeps nested workspace paths visible for legacy repos without explicit visibility', () => {
const repo = makeRepo()
const settings = makeSettings()
const detected = toDetectedWorktree({
repo,
settings,
worktree: makeWorktree({
path: '/orca/workspaces/app/manual-git-worktree',
isMainWorktree: false
}),
knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo)
})
expect(detected.ownership).toBe('external')
expect(detected.visible).toBe(true)
})
it('hides metadata-free nested workspace paths for legacy repos that hide external worktrees', () => {
const repo = makeRepo({
externalWorktreeVisibility: 'hide',
externalWorktreeVisibilityLegacy: true
})
const settings = makeSettings()
const detected = toDetectedWorktree({
repo,
settings,
worktree: makeWorktree({
path: '/orca/workspaces/app/manual-git-worktree',
isMainWorktree: false
}),
knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo)
})
expect(detected.ownership).toBe('external')
expect(detected.visible).toBe(false)
})
it('treats flat workspace-root descendants as unknown legacy without strong metadata', () => {
const repo = makeRepo()
const settings = makeSettings({ nestWorkspaces: false })
@ -172,7 +260,7 @@ describe('worktree ownership classification', () => {
worktree: makeWorktree({ path: '/old/workspaces/app/feature' }),
knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo)
})
).toBe('orca-managed')
).toBe('external')
})
it('builds known layouts from large workspace history lists', () => {
@ -214,7 +302,7 @@ describe('worktree ownership classification', () => {
}),
knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo)
})
).toBe('orca-managed')
).toBe('external')
})
it('keeps selected linked checkouts visible without trusting Git main-worktree', () => {

View File

@ -1,5 +1,4 @@
import {
getRuntimePathBasename,
isRuntimePathAbsolute,
isWindowsAbsolutePathLike,
normalizeRuntimePathForComparison,
@ -160,15 +159,13 @@ export function classifyWorktreeOwnership(args: {
return 'orca-managed'
}
if (matchesStrongOrcaCreatePath(args.worktree.path, args.knownOrcaLayouts, args.repo)) {
return 'orca-managed'
}
if (isUnderFlatOrUntrustedOrcaRoot(args.worktree.path, args.knownOrcaLayouts)) {
return 'unknown-legacy'
}
if (canClassifyAsExternal(args.worktree.path, args.knownOrcaLayouts)) {
// Why: a plain `git worktree add` can target Orca's nested workspace
// folder. Only metadata proves Orca created it.
return 'external'
}
@ -240,6 +237,7 @@ export function areRuntimePathsEqual(leftPath: string, rightPath: string): boole
function hasStrongOrcaMetadata(meta: WorktreeMeta | undefined): boolean {
return Boolean(
meta?.orcaCreatedAt ||
meta?.orcaCreationWorkspaceLayout ||
meta?.createdAt ||
meta?.createdWithAgent ||
meta?.pushTarget ||
@ -249,38 +247,6 @@ function hasStrongOrcaMetadata(meta: WorktreeMeta | undefined): boolean {
)
}
export function matchesStrongOrcaCreatePath(
worktreePath: string,
knownOrcaLayouts: readonly OrcaWorkspaceLayout[],
repo: Pick<Repo, 'path'>
): boolean {
const repoName = getRuntimePathBasename(repo.path).replace(/\.git$/i, '')
if (!repoName) {
return false
}
for (const layout of knownOrcaLayouts) {
if (!layout.nestWorkspaces) {
continue
}
const relative = relativePathInsideRoot(layout.path, worktreePath)
if (relative === null) {
continue
}
const segments = splitNormalizedPath(relative)
const caseInsensitive =
isWindowsAbsolutePathLike(layout.path) || isWindowsAbsolutePathLike(worktreePath)
if (
segments.length === 2 &&
normalizePathSegment(segments[0], caseInsensitive) ===
normalizePathSegment(repoName, caseInsensitive) &&
segments[1].length > 0
) {
return true
}
}
return false
}
function isUnderFlatOrUntrustedOrcaRoot(
worktreePath: string,
knownOrcaLayouts: OrcaWorkspaceLayout[]
@ -313,12 +279,3 @@ function canClassifyAsExternal(
}
return true
}
function splitNormalizedPath(value: string): string[] {
return normalizeRuntimePathSeparators(value).split('/').filter(Boolean)
}
function normalizePathSegment(value: string, caseInsensitive: boolean): string {
const normalized = normalizeRuntimePathSeparators(value)
return caseInsensitive ? normalized.toLowerCase() : normalized
}