mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 16:02:35 +00:00
125 lines
4.8 KiB
TypeScript
125 lines
4.8 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import type { Repo } from '../shared/repo-types'
|
|
import type { WorkspaceLineage, WorktreeLineage } from '../shared/worktree/lineage-types'
|
|
import type { GitWorktreeInfo } from '../shared/worktree/types'
|
|
import { getRepoExecutionHostId } from '../shared/execution-host'
|
|
import { isWorkspaceKey, parseWorkspaceKey, worktreeWorkspaceKey } from '../shared/workspace-scope'
|
|
import { splitWorktreeId } from '../shared/worktree/id'
|
|
import { worktreeRetentionPathComparisonKey } from './worktree-retention-path-comparison'
|
|
import type { Store } from './persistence'
|
|
|
|
function worktreeIdBelongsToRepo(worktreeId: string, repoPrefix: string): boolean {
|
|
return worktreeId.startsWith(repoPrefix)
|
|
}
|
|
|
|
function workspaceKeyBelongsToRepo(workspaceKey: string, repoPrefix: string): boolean {
|
|
const scope = parseWorkspaceKey(workspaceKey)
|
|
return scope?.type === 'worktree' && worktreeIdBelongsToRepo(scope.worktreeId, repoPrefix)
|
|
}
|
|
|
|
function hasStoredRepoLineage(
|
|
repoPrefix: string,
|
|
worktreeLineage: Readonly<Record<string, WorktreeLineage>>,
|
|
workspaceLineage: Readonly<Record<string, WorkspaceLineage>>
|
|
): boolean {
|
|
return (
|
|
Object.entries(worktreeLineage).some(
|
|
([childId, lineage]) =>
|
|
worktreeIdBelongsToRepo(childId, repoPrefix) ||
|
|
worktreeIdBelongsToRepo(lineage.parentWorktreeId, repoPrefix)
|
|
) ||
|
|
Object.values(workspaceLineage).some(
|
|
(lineage) =>
|
|
workspaceKeyBelongsToRepo(lineage.childWorkspaceKey, repoPrefix) ||
|
|
workspaceKeyBelongsToRepo(lineage.parentWorkspaceKey, repoPrefix)
|
|
)
|
|
)
|
|
}
|
|
|
|
export function pruneLineageForMissingRepoWorktrees(
|
|
store: Store,
|
|
repo: Repo,
|
|
gitWorktrees: GitWorktreeInfo[],
|
|
options: {
|
|
platform?: NodeJS.Platform
|
|
preservedMetadataCandidateIds?: ReadonlySet<string>
|
|
} = {}
|
|
): void {
|
|
const { platform = process.platform, preservedMetadataCandidateIds } = options
|
|
if (
|
|
typeof store.getAllWorktreeLineage !== 'function' ||
|
|
typeof store.removeWorktreeLineage !== 'function'
|
|
) {
|
|
return
|
|
}
|
|
const worktreeLineage = store.getAllWorktreeLineage()
|
|
const workspaceLineage = store.getAllWorkspaceLineage?.() ?? {}
|
|
const repoPrefix = `${repo.id}::`
|
|
// Why: one empty observation cannot prove every registered lineage edge disappeared at once.
|
|
if (
|
|
gitWorktrees.length === 0 &&
|
|
hasStoredRepoLineage(repoPrefix, worktreeLineage, workspaceLineage)
|
|
) {
|
|
return
|
|
}
|
|
const liveIds = new Set(gitWorktrees.map((worktree) => `${repo.id}::${worktree.path}`))
|
|
const livePathKeys = new Set(
|
|
[repo.path, ...gitWorktrees.map(({ path }) => path)].map((pathValue) =>
|
|
worktreeRetentionPathComparisonKey(pathValue, platform)
|
|
)
|
|
)
|
|
const isLive = (worktreeId: string): boolean => {
|
|
if (liveIds.has(worktreeId) || preservedMetadataCandidateIds?.has(worktreeId)) {
|
|
return true
|
|
}
|
|
const worktreePath = splitWorktreeId(worktreeId)?.worktreePath
|
|
if (!worktreePath) {
|
|
return false
|
|
}
|
|
if (livePathKeys.has(worktreeRetentionPathComparisonKey(worktreePath, platform))) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
const expectedHostId = getRepoExecutionHostId(repo)
|
|
const repoOwners = store.getRepos().filter((candidate) => candidate.id === repo.id)
|
|
const canMutateWorktree = (worktreeId: string): boolean => {
|
|
const hostId = store.getWorktreeMeta(worktreeId)?.hostId
|
|
return hostId ? hostId === expectedHostId : repoOwners.length === 1
|
|
}
|
|
for (const childWorkspaceKey of Object.keys(workspaceLineage)) {
|
|
const childScope = parseWorkspaceKey(childWorkspaceKey)
|
|
if (
|
|
childScope?.type === 'worktree' &&
|
|
worktreeIdBelongsToRepo(childScope.worktreeId, repoPrefix) &&
|
|
canMutateWorktree(childScope.worktreeId) &&
|
|
!isLive(childScope.worktreeId) &&
|
|
isWorkspaceKey(childWorkspaceKey)
|
|
) {
|
|
store.removeWorkspaceLineage?.(childWorkspaceKey)
|
|
}
|
|
}
|
|
for (const [childId, lineage] of Object.entries(worktreeLineage)) {
|
|
if (
|
|
worktreeIdBelongsToRepo(childId, repoPrefix) &&
|
|
canMutateWorktree(childId) &&
|
|
!isLive(childId)
|
|
) {
|
|
// Why: a proven-missing path must not transfer its lineage to a future checkout at that path.
|
|
store.removeWorktreeLineage(childId)
|
|
store.removeWorkspaceLineage?.(worktreeWorkspaceKey(childId))
|
|
}
|
|
if (
|
|
worktreeIdBelongsToRepo(lineage.parentWorktreeId, repoPrefix) &&
|
|
canMutateWorktree(lineage.parentWorktreeId) &&
|
|
!isLive(lineage.parentWorktreeId)
|
|
) {
|
|
const parentMeta = store.getWorktreeMeta(lineage.parentWorktreeId)
|
|
if (parentMeta?.instanceId === lineage.parentWorktreeInstanceId) {
|
|
// Why: rotate a proven-missing parent's identity once so path reuse cannot validate old lineage.
|
|
store.setWorktreeMeta(lineage.parentWorktreeId, { instanceId: randomUUID() })
|
|
}
|
|
}
|
|
}
|
|
}
|