mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
Resolve workspace lineage parents in one shared module (#38835)
Inert refactor: move the same-store parent rule and merged resolver into src/shared/worktree/workspace-lineage-graph.ts and add one row identity rule. No behaviour change.
This commit is contained in:
@@ -50,17 +50,20 @@ function getCrossStoreLineageParents(
|
||||
worktrees: readonly Worktree[],
|
||||
stores: MobileLineageStores
|
||||
): Map<string, Worktree> {
|
||||
// Folder workspaces never nest rows here, so their cross-machine children stay flat as before.
|
||||
const resolutionRows = new Map(
|
||||
worktrees.map((worktree) => [
|
||||
getWorktreeRowIdentity(worktree),
|
||||
{
|
||||
worktree,
|
||||
id: worktree.worktreeId,
|
||||
// Rows the host lists without a host id are its own: only relayed server rows name one.
|
||||
hostId: worktree.hostId ?? LOCAL_EXECUTION_HOST_ID,
|
||||
instanceId: worktree.worktreeInstanceId
|
||||
}
|
||||
])
|
||||
worktrees
|
||||
.filter((worktree) => worktree.workspaceKind !== 'folder-workspace')
|
||||
.map((worktree) => [
|
||||
getWorktreeRowIdentity(worktree),
|
||||
{
|
||||
worktree,
|
||||
id: worktree.worktreeId,
|
||||
// Rows the host lists without a host id are its own: only relayed server rows name one.
|
||||
hostId: worktree.hostId ?? LOCAL_EXECUTION_HOST_ID,
|
||||
instanceId: worktree.worktreeInstanceId
|
||||
}
|
||||
])
|
||||
)
|
||||
if (![...resolutionRows.values()].some((row) => row.worktree.crossStoreParent)) {
|
||||
return new Map()
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import type {
|
||||
WorkspaceStatusDefinition,
|
||||
Worktree
|
||||
} from '../../../../../../shared/worktree/types'
|
||||
import { getProjectedWorktreeLineage } from '../../../../../../shared/sidebar-list/worktree-lineage-projection'
|
||||
import { getProjectedWorktreeLineage } from '../../../../../../shared/worktree/workspace-lineage-graph'
|
||||
import '../../workspace-status/presentation/workspace-status'
|
||||
import { getWorkspaceStatus } from '../../../../../../shared/workspace-statuses'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import {
|
||||
getCyclicLineageChildKeys,
|
||||
getCyclicWorktreeLineageChildIds,
|
||||
isValidResolvedWorktreeLineageEdge
|
||||
} from '../resolved-worktree-lineage'
|
||||
import type { ExecutionHostId } from '../execution-host'
|
||||
import {
|
||||
composeWorktreeHostIdentity,
|
||||
getWorktreeHostIdentity
|
||||
} from '../worktree/host-qualified-identity'
|
||||
import { getWorktreeHostIdentity } from '../worktree/host-qualified-identity'
|
||||
import type { WorktreeLineage } from '../worktree/lineage-types'
|
||||
import type { CrossStoreLineageView } from '../worktree/cross-store-lineage-resolution'
|
||||
import {
|
||||
resolveCrossStoreLineageParents,
|
||||
type CrossStoreLineageView
|
||||
} from '../worktree/cross-store-lineage-resolution'
|
||||
getProjectedWorktreeLineage,
|
||||
getSameStoreLineageParent,
|
||||
resolveWorkspaceLineageParents,
|
||||
type WorktreeWithInlineLineage
|
||||
} from '../worktree/workspace-lineage-graph'
|
||||
import type { Worktree } from '../worktree/types'
|
||||
|
||||
export type LineageRenderInfo =
|
||||
@@ -20,18 +19,6 @@ export type LineageRenderInfo =
|
||||
| { state: 'valid'; lineage: WorktreeLineage; parent: Worktree }
|
||||
| { state: 'missing'; lineage: WorktreeLineage }
|
||||
|
||||
type WorktreeWithResolvedLineage = Worktree & { lineage?: WorktreeLineage | null }
|
||||
|
||||
export function getProjectedWorktreeLineage(
|
||||
worktree: Worktree,
|
||||
lineageById: Readonly<Record<string, WorktreeLineage>>
|
||||
): WorktreeLineage | null | undefined {
|
||||
if (Object.hasOwn(lineageById, worktree.id)) {
|
||||
return lineageById[worktree.id]
|
||||
}
|
||||
return (worktree as WorktreeWithResolvedLineage).lineage
|
||||
}
|
||||
|
||||
type LineageProjection = {
|
||||
cyclicLineageIds?: Set<string>
|
||||
childrenByParentId?: Map<string, Worktree[]>
|
||||
@@ -148,7 +135,7 @@ export function getHostScopedWorktreeLineageInputs(
|
||||
worktreeMap.set(worktree.id, worktree)
|
||||
const projected = lineageById[worktree.id]
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: resolved rows may carry an inline lineage the Worktree type omits; it is only read, never trusted over a matching projection.
|
||||
const inline = (worktree as WorktreeWithResolvedLineage).lineage
|
||||
const inline = (worktree as WorktreeWithInlineLineage).lineage
|
||||
const lineage = projected?.worktreeInstanceId === worktree.instanceId ? projected : inline
|
||||
if (lineage) {
|
||||
hostLineageById[worktree.id] = lineage
|
||||
@@ -178,55 +165,9 @@ export function getWorktreeLineageAncestors(
|
||||
return ancestors
|
||||
}
|
||||
|
||||
/** Nest on the worktree’s own host; legacy rows without host IDs nest only under equally unqualified parents. */
|
||||
export function getSidebarLineageParent(
|
||||
worktree: Worktree,
|
||||
lineageById: Readonly<Record<string, WorktreeLineage>>,
|
||||
rowsByHostIdentity: ReadonlyMap<string, Worktree>
|
||||
): Worktree | undefined {
|
||||
const projected = getProjectedWorktreeLineage(worktree, lineageById)
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: resolved rows may carry an inline lineage the Worktree type omits; it is only read, never trusted over a matching projection.
|
||||
const inline = (worktree as WorktreeWithResolvedLineage).lineage
|
||||
const lineage = projected?.worktreeInstanceId === worktree.instanceId ? projected : inline
|
||||
if (!lineage) {
|
||||
return undefined
|
||||
}
|
||||
const parent = rowsByHostIdentity.get(
|
||||
composeWorktreeHostIdentity(worktree.hostId, lineage.parentWorktreeId)
|
||||
)
|
||||
return parent && isValidResolvedWorktreeLineageEdge(worktree, parent, lineage)
|
||||
? parent
|
||||
: undefined
|
||||
}
|
||||
|
||||
/** Child host identity → parent host identity, resolved once over every visible store's complete rows. */
|
||||
export type SidebarLineageParentIdentities = ReadonlyMap<string, string>
|
||||
|
||||
function resolveParentsByIdentity(
|
||||
lineageById: Readonly<Record<string, WorktreeLineage>>,
|
||||
rowsByHostIdentity: ReadonlyMap<string, Worktree>,
|
||||
crossStoreLineage?: CrossStoreLineageView
|
||||
): Map<string, Worktree> {
|
||||
const parentByIdentity = crossStoreLineage
|
||||
? resolveCrossStoreLineageParents(rowsByHostIdentity, crossStoreLineage)
|
||||
: new Map<string, Worktree>()
|
||||
for (const [identity, row] of rowsByHostIdentity) {
|
||||
const parent = getSidebarLineageParent(row, lineageById, rowsByHostIdentity)
|
||||
if (parent) {
|
||||
parentByIdentity.set(identity, parent)
|
||||
}
|
||||
}
|
||||
const cyclic = getCyclicLineageChildKeys(
|
||||
new Map(
|
||||
[...parentByIdentity].map(([identity, parent]) => [identity, getWorktreeHostIdentity(parent)])
|
||||
)
|
||||
)
|
||||
for (const identity of cyclic) {
|
||||
parentByIdentity.delete(identity)
|
||||
}
|
||||
return parentByIdentity
|
||||
}
|
||||
|
||||
/** Same-store edges merged with cross-store records (a valid same-store edge wins), cycles dropped.
|
||||
* Pass every visible store's unfiltered rows: a filtered-out twin must still make a parent ambiguous. */
|
||||
export function resolveSidebarLineageParentIdentities(
|
||||
@@ -235,9 +176,9 @@ export function resolveSidebarLineageParentIdentities(
|
||||
crossStoreLineage: CrossStoreLineageView
|
||||
): SidebarLineageParentIdentities {
|
||||
return new Map(
|
||||
[...resolveParentsByIdentity(lineageById, completeRowsByHostIdentity, crossStoreLineage)].map(
|
||||
([identity, parent]) => [identity, getWorktreeHostIdentity(parent)]
|
||||
)
|
||||
[
|
||||
...resolveWorkspaceLineageParents(lineageById, completeRowsByHostIdentity, crossStoreLineage)
|
||||
].map(([identity, parent]) => [identity, getWorktreeHostIdentity(parent)])
|
||||
)
|
||||
}
|
||||
|
||||
@@ -256,7 +197,7 @@ export function getSidebarLineageParentsByIdentity(
|
||||
return cached
|
||||
}
|
||||
if (!parentIdentities) {
|
||||
const parentByIdentity = resolveParentsByIdentity(lineageById, rowsByHostIdentity)
|
||||
const parentByIdentity = resolveWorkspaceLineageParents(lineageById, rowsByHostIdentity)
|
||||
projection.sidebarParentByIdentity = parentByIdentity
|
||||
return parentByIdentity
|
||||
}
|
||||
@@ -284,7 +225,7 @@ export function getSidebarLineageAncestors(
|
||||
const seen = new Set([getWorktreeHostIdentity(worktree)])
|
||||
let current = worktree
|
||||
while (!cyclicLineageIds.has(current.id)) {
|
||||
const parent = getSidebarLineageParent(current, lineageById, rowsByHostIdentity)
|
||||
const parent = getSameStoreLineageParent(current, lineageById, rowsByHostIdentity)
|
||||
if (!parent || seen.has(getWorktreeHostIdentity(parent))) {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
resolveSidebarLineageParentIdentities
|
||||
} from '../sidebar-list/worktree-lineage-projection'
|
||||
import { getWorktreeHostIdentity } from './host-qualified-identity'
|
||||
import type { CrossStoreLineageView, LineageStoreView } from './cross-store-lineage-resolution'
|
||||
import {
|
||||
resolveCrossStoreLineageParents,
|
||||
type CrossStoreLineageView,
|
||||
type LineageStoreView
|
||||
} from './cross-store-lineage-resolution'
|
||||
import type { CrossStoreWorkspaceLineage, WorktreeLineage } from './lineage-types'
|
||||
import type { Worktree } from './types'
|
||||
|
||||
@@ -133,6 +137,31 @@ describe('cross-store parent resolution', () => {
|
||||
).toEqual({})
|
||||
})
|
||||
|
||||
it('resolves a folder workspace parent by its folder id', () => {
|
||||
const { instanceId: _none, ...folderRow } = row('folder:3b8f6c2e', '')
|
||||
const folderRecords = {
|
||||
'worktree:repo9::/work/fix-login-tests': {
|
||||
...records['worktree:repo9::/work/fix-login-tests'],
|
||||
parent: {
|
||||
installationId: MAC_INSTALL,
|
||||
workspaceKey: 'folder:3b8f6c2e',
|
||||
instanceId: '3b8f6c2e'
|
||||
}
|
||||
}
|
||||
}
|
||||
const rows = new Map<string, Worktree>(
|
||||
[folderRow, serverChild].map((worktree) => [getWorktreeHostIdentity(worktree), worktree])
|
||||
)
|
||||
const parents = resolveCrossStoreLineageParents(
|
||||
rows,
|
||||
new Map([
|
||||
['', store(MAC_INSTALL)],
|
||||
['build-box', store(SERVER_INSTALL, folderRecords)]
|
||||
])
|
||||
)
|
||||
expect([...parents.values()].map((parent) => parent.id)).toEqual(['folder:3b8f6c2e'])
|
||||
})
|
||||
|
||||
it('resolves within the child store when both ends share it; a same-store edge wins', () => {
|
||||
const serverParent = row('repo9::/work/fix-login', 'parent-7c1e', 'build-box')
|
||||
const view = new Map([
|
||||
|
||||
@@ -4,8 +4,9 @@ import {
|
||||
lastVerifiedRuntimeStatus,
|
||||
type RuntimeEnvironmentStatus
|
||||
} from '../remote-runtime/host-status/runtime-host-status'
|
||||
import { worktreeWorkspaceKey, type WorkspaceKey } from '../workspace-scope'
|
||||
import type { WorkspaceKey } from '../workspace-scope'
|
||||
import { isCrossStoreWorkspaceLineage, validCrossStoreLineage } from './cross-store-lineage-records'
|
||||
import { lineageInstanceIdOfRow, workspaceKeyOfRow } from './lineage-row-identity'
|
||||
import type { Worktree } from './types'
|
||||
|
||||
/** The viewer's own store; other stores are keyed by their paired runtime environment id. */
|
||||
@@ -36,11 +37,6 @@ export function lineageStoreKeyOfRow(row: LineageStoreRow): string | undefined {
|
||||
return host.kind === 'runtime' ? host.environmentId : OWN_LINEAGE_STORE_KEY
|
||||
}
|
||||
|
||||
function workspaceKeyOfRow(row: Pick<Worktree, 'id'>): WorkspaceKey {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: folder rows carry their folder WorkspaceKey as id (folderWorkspaceToWorktree).
|
||||
return row.id.startsWith('folder:') ? (row.id as WorkspaceKey) : worktreeWorkspaceKey(row.id)
|
||||
}
|
||||
|
||||
/** Paired stores are identified by the installationId each server last reported. An offline server keeps
|
||||
* its last-known identity; a removed, retired or refusing one is left out, so it neither nests nor parents. */
|
||||
export function buildCrossStoreLineageView(args: {
|
||||
@@ -109,7 +105,8 @@ export function resolveCrossStoreLineageParents<Row extends LineageResolutionRow
|
||||
const childRowsByStoreAndKey = new Map<string, Map<WorkspaceKey, [string, Row][]>>()
|
||||
const rowsByStoreAndInstance = new Map<string, Map<string, Row[]>>()
|
||||
for (const [identity, row] of rowsByIdentity) {
|
||||
if (!row.instanceId) {
|
||||
const instanceId = lineageInstanceIdOfRow(row)
|
||||
if (!instanceId) {
|
||||
continue
|
||||
}
|
||||
const storeKey = lineageStoreKeyOfRow(row)
|
||||
@@ -125,7 +122,7 @@ export function resolveCrossStoreLineageParents<Row extends LineageResolutionRow
|
||||
rowsByStoreAndInstance.set(storeKey, byInstance)
|
||||
}
|
||||
pushTo(byKey, workspaceKeyOfRow(row), [identity, row])
|
||||
pushTo(byInstance, row.instanceId, row)
|
||||
pushTo(byInstance, instanceId, row)
|
||||
}
|
||||
const storeKeysByInstallationId = new Map<string, string[]>()
|
||||
for (const [storeKey, store] of view) {
|
||||
@@ -147,7 +144,7 @@ export function resolveCrossStoreLineageParents<Row extends LineageResolutionRow
|
||||
childRowsByStoreAndKey
|
||||
.get(childStoreKey)
|
||||
?.get(lineage.childWorkspaceKey)
|
||||
?.filter(([, row]) => row.instanceId === lineage.childInstanceId)
|
||||
?.filter(([, row]) => lineageInstanceIdOfRow(row) === lineage.childInstanceId)
|
||||
)
|
||||
// Two stores reporting one installationId are a copied profile; neither is the parent's.
|
||||
const parentStoreKey = soleRow(storeKeysByInstallationId.get(lineage.parent.installationId))
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { parseWorkspaceKey, worktreeWorkspaceKey, type WorkspaceKey } from '../workspace-scope'
|
||||
import type { Worktree } from './types'
|
||||
|
||||
export function workspaceKeyOfRow(row: Pick<Worktree, 'id'>): WorkspaceKey {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: folder rows carry their folder WorkspaceKey as id (folderWorkspaceToWorktree).
|
||||
return row.id.startsWith('folder:') ? (row.id as WorkspaceKey) : worktreeWorkspaceKey(row.id)
|
||||
}
|
||||
|
||||
/** A row's cross-store instance identity. A folder workspace's id is minted per creation and never
|
||||
* reused, so it is the folder's instance; folder rows carry no instanceId of their own. */
|
||||
export function lineageInstanceIdOfRow(
|
||||
row: Pick<Worktree, 'id' | 'instanceId'>
|
||||
): string | undefined {
|
||||
const scope = parseWorkspaceKey(row.id)
|
||||
return scope?.type === 'folder' ? scope.folderWorkspaceId : row.instanceId || undefined
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
getCyclicLineageChildKeys,
|
||||
isValidResolvedWorktreeLineageEdge
|
||||
} from '../resolved-worktree-lineage'
|
||||
import { composeWorktreeHostIdentity, getWorktreeHostIdentity } from './host-qualified-identity'
|
||||
import type { WorktreeLineage } from './lineage-types'
|
||||
import {
|
||||
resolveCrossStoreLineageParents,
|
||||
type CrossStoreLineageView
|
||||
} from './cross-store-lineage-resolution'
|
||||
import type { Worktree } from './types'
|
||||
|
||||
export type WorktreeWithInlineLineage = Worktree & { lineage?: WorktreeLineage | null }
|
||||
|
||||
export function getProjectedWorktreeLineage(
|
||||
worktree: Worktree,
|
||||
lineageById: Readonly<Record<string, WorktreeLineage>>
|
||||
): WorktreeLineage | null | undefined {
|
||||
if (Object.hasOwn(lineageById, worktree.id)) {
|
||||
return lineageById[worktree.id]
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: resolved rows may carry an inline lineage the Worktree type omits; it is only read.
|
||||
return (worktree as WorktreeWithInlineLineage).lineage
|
||||
}
|
||||
|
||||
/** Nest on the worktree’s own host; legacy rows without host IDs nest only under equally unqualified parents. */
|
||||
export function getSameStoreLineageParent(
|
||||
worktree: Worktree,
|
||||
lineageById: Readonly<Record<string, WorktreeLineage>>,
|
||||
rowsByHostIdentity: ReadonlyMap<string, Worktree>
|
||||
): Worktree | undefined {
|
||||
const projected = getProjectedWorktreeLineage(worktree, lineageById)
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: resolved rows may carry an inline lineage the Worktree type omits; it is only read, never trusted over a matching projection.
|
||||
const inline = (worktree as WorktreeWithInlineLineage).lineage
|
||||
const lineage = projected?.worktreeInstanceId === worktree.instanceId ? projected : inline
|
||||
if (!lineage) {
|
||||
return undefined
|
||||
}
|
||||
const parent = rowsByHostIdentity.get(
|
||||
composeWorktreeHostIdentity(worktree.hostId, lineage.parentWorktreeId)
|
||||
)
|
||||
return parent && isValidResolvedWorktreeLineageEdge(worktree, parent, lineage)
|
||||
? parent
|
||||
: undefined
|
||||
}
|
||||
|
||||
/** Every parent the viewer can see, by child host identity: same-store edges from saved lineage merged
|
||||
* with cross-store records (a valid same-store edge wins), cycles dropped. Rows may include folder
|
||||
* workspace rows; their identity is lineageInstanceIdOfRow. Pass every visible store's unfiltered rows:
|
||||
* a filtered-out twin must still make a parent ambiguous. */
|
||||
export function resolveWorkspaceLineageParents(
|
||||
lineageById: Readonly<Record<string, WorktreeLineage>>,
|
||||
rowsByHostIdentity: ReadonlyMap<string, Worktree>,
|
||||
crossStoreLineage?: CrossStoreLineageView
|
||||
): Map<string, Worktree> {
|
||||
const parentByIdentity = crossStoreLineage
|
||||
? resolveCrossStoreLineageParents(rowsByHostIdentity, crossStoreLineage)
|
||||
: new Map<string, Worktree>()
|
||||
for (const [identity, row] of rowsByHostIdentity) {
|
||||
const parent = getSameStoreLineageParent(row, lineageById, rowsByHostIdentity)
|
||||
if (parent) {
|
||||
parentByIdentity.set(identity, parent)
|
||||
}
|
||||
}
|
||||
const cyclic = getCyclicLineageChildKeys(
|
||||
new Map(
|
||||
[...parentByIdentity].map(([identity, parent]) => [identity, getWorktreeHostIdentity(parent)])
|
||||
)
|
||||
)
|
||||
for (const identity of cyclic) {
|
||||
parentByIdentity.delete(identity)
|
||||
}
|
||||
return parentByIdentity
|
||||
}
|
||||
Reference in New Issue
Block a user