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:
Jinwoo Hong
2026-11-06 00:49:05 -05:00
committed by GitHub
parent ea421720b9
commit a57c19aa91
7 changed files with 153 additions and 93 deletions
+13 -10
View File
@@ -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()
@@ -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
}