perf: add versioned git repository snapshots

This commit is contained in:
Neil
2026-07-30 04:28:48 -07:00
parent b680c59da6
commit 98134f6f7b
13 changed files with 1607 additions and 201 deletions
@@ -0,0 +1,388 @@
import { describe, expect, it, vi } from 'vitest'
import type { GitPushTarget, GitStatusResult, GitUpstreamStatus } from '../../shared/types'
import {
GitRepositorySnapshotOwner,
type GitRepositoryExecutionIdentity,
type GitRepositoryStatusIdentity
} from './git-repository-snapshot-owner'
import { MAX_GIT_REPOSITORY_SNAPSHOT_STATUS_ENTRIES } from './git-repository-snapshot-projection'
const native = { kind: 'native' } as const
const defaultStatusIdentity: GitRepositoryStatusIdentity = {
includeIgnored: false,
reuseLineStats: false,
bypassEffectiveUpstreamNegativeCache: false,
limit: 1_000,
sharedLinkPaths: []
}
const statusResult: GitStatusResult = {
entries: [{ path: 'src/app.ts', status: 'modified', area: 'unstaged', added: 2, removed: 1 }],
conflictOperation: 'merge',
head: 'abc123',
branch: 'refs/heads/main',
upstreamStatus: {
hasUpstream: true,
upstreamName: 'origin/main',
ahead: 1,
behind: 2
}
}
function deferred<T>(): {
promise: Promise<T>
resolve: (value: T) => void
reject: (error: unknown) => void
} {
let resolve!: (value: T) => void
let reject!: (error: unknown) => void
const promise = new Promise<T>((innerResolve, innerReject) => {
resolve = innerResolve
reject = innerReject
})
return { promise, resolve, reject }
}
function query(
executionIdentity: GitRepositoryExecutionIdentity = native,
worktreePath = '/repo',
identity: GitRepositoryStatusIdentity = defaultStatusIdentity,
pushTarget?: GitPushTarget
) {
return { executionIdentity, worktreePath, statusIdentity: identity, pushTarget }
}
describe('GitRepositorySnapshotOwner', () => {
it('publishes immutable combined projections with monotonic revisions', async () => {
const now = vi.fn().mockReturnValueOnce(100).mockReturnValueOnce(200)
const owner = new GitRepositorySnapshotOwner(128, 8, now)
await owner.readStatus(
native,
'/repo',
defaultStatusIdentity,
undefined,
async () => statusResult
)
const first = owner.getSnapshot(query())
expect(first).toMatchObject({
revision: 1,
generatedAt: 100,
repositoryIdentity: { head: 'abc123', branch: 'refs/heads/main' },
status: {
entries: statusResult.entries,
didHitLimit: false,
statusLength: null,
lineStatsState: 'complete'
},
upstream: statusResult.upstreamStatus,
conflicts: 'merge',
worktreeGraphVersion: 0,
freshness: {
status: { state: 'fresh', generation: 0, revision: 1 },
upstream: { state: 'fresh', generation: 0, revision: 1 },
worktreeGraph: { state: 'placeholder', generation: 0 }
}
})
expect(Object.isFrozen(first)).toBe(true)
expect(Object.isFrozen(first?.status)).toBe(true)
expect(Object.isFrozen(first?.status.entries)).toBe(true)
expect(Object.isFrozen(first?.status.entries[0])).toBe(true)
const refreshedUpstream = {
hasUpstream: true,
upstreamName: 'origin/main',
ahead: 3,
behind: 0,
behindCommitsArePatchEquivalent: true
}
await owner.readUpstream(native, '/repo', undefined, async () => refreshedUpstream)
const second = owner.getSnapshot(query())
expect(second?.revision).toBe(2)
expect(second?.generatedAt).toBe(200)
expect(second?.status.entries).toEqual(first?.status.entries)
expect(second?.upstream).toEqual(refreshedUpstream)
expect(second?.freshness.status.revision).toBe(1)
expect(second?.freshness.upstream.revision).toBe(2)
})
it('does not publish rejected reads and always retries settled work', async () => {
const owner = new GitRepositorySnapshotOwner()
const failure = new Error('read failed')
const statusLoad = vi
.fn<(_signal: AbortSignal) => Promise<GitStatusResult>>()
.mockRejectedValueOnce(failure)
.mockResolvedValueOnce(statusResult)
.mockResolvedValueOnce(statusResult)
.mockRejectedValueOnce(failure)
const upstreamLoad = vi
.fn<() => Promise<GitUpstreamStatus>>()
.mockRejectedValueOnce(failure)
.mockResolvedValueOnce({ hasUpstream: false, ahead: 0, behind: 0 })
.mockResolvedValueOnce({ hasUpstream: false, ahead: 0, behind: 0 })
.mockRejectedValueOnce(failure)
await expect(
owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, statusLoad)
).rejects.toBe(failure)
await expect(owner.readUpstream(native, '/repo', undefined, upstreamLoad)).rejects.toBe(failure)
expect(owner.getSnapshot(query())).toBeNull()
expect(owner.getRetentionState().repositories).toBe(0)
await owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, statusLoad)
await owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, statusLoad)
await owner.readUpstream(native, '/repo', undefined, upstreamLoad)
await owner.readUpstream(native, '/repo', undefined, upstreamLoad)
expect(statusLoad).toHaveBeenCalledTimes(3)
expect(upstreamLoad).toHaveBeenCalledTimes(3)
expect(owner.getSnapshot(query())?.revision).toBe(4)
await expect(
owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, statusLoad)
).rejects.toBe(failure)
await expect(owner.readUpstream(native, '/repo', undefined, upstreamLoad)).rejects.toBe(failure)
expect(owner.getSnapshot(query())).toMatchObject({
revision: 4,
freshness: {
status: { state: 'failed' },
upstream: { state: 'failed' }
}
})
})
it('keeps status cancellation scoped to each lease', async () => {
const owner = new GitRepositorySnapshotOwner()
const pending = deferred<GitStatusResult>()
const load = vi.fn((_signal: AbortSignal) => pending.promise)
const firstController = new AbortController()
const secondController = new AbortController()
const firstError = new Error('first cancelled')
const first = owner.readStatus(
native,
'/repo',
defaultStatusIdentity,
firstController.signal,
load
)
const second = owner.readStatus(
native,
'/repo',
defaultStatusIdentity,
secondController.signal,
load
)
firstController.abort(firstError)
await expect(first).rejects.toBe(firstError)
expect(load).toHaveBeenCalledOnce()
expect(load.mock.calls[0][0].aborted).toBe(false)
pending.resolve(statusResult)
await expect(second).resolves.toBe(statusResult)
expect(owner.getSnapshot(query())?.revision).toBe(1)
})
it('prefers a newer embedded upstream projection after invalidation', async () => {
const owner = new GitRepositorySnapshotOwner()
await owner.readUpstream(native, '/repo', undefined, async () => ({
hasUpstream: true,
upstreamName: 'old/main',
ahead: 0,
behind: 0
}))
owner.invalidate()
await owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, async () => ({
...statusResult,
upstreamStatus: {
hasUpstream: true,
upstreamName: 'fresh/main',
ahead: 1,
behind: 0
}
}))
expect(owner.getSnapshot(query())).toMatchObject({
upstream: { upstreamName: 'fresh/main' },
freshness: { upstream: { state: 'fresh', generation: 1 } }
})
})
it('suppresses stale status and upstream completion across both mutation fences', async () => {
const owner = new GitRepositorySnapshotOwner()
const statuses = Array.from({ length: 3 }, () => deferred<GitStatusResult>())
const upstreams = Array.from({ length: 3 }, () => deferred<GitUpstreamStatus>())
let statusIndex = 0
let upstreamIndex = 0
const readStatus = () =>
owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, () => {
const pending = statuses[statusIndex]
statusIndex += 1
return pending.promise
})
const readUpstream = () =>
owner.readUpstream(native, '/repo', undefined, () => {
const pending = upstreams[upstreamIndex]
upstreamIndex += 1
return pending.promise
})
const before = [readStatus(), readUpstream()]
await Promise.resolve()
owner.invalidate()
const during = [readStatus(), readUpstream()]
await Promise.resolve()
owner.invalidate()
const after = [readStatus(), readUpstream()]
await Promise.resolve()
statuses[0].resolve({ ...statusResult, head: 'before' })
statuses[1].resolve({ ...statusResult, head: 'during' })
statuses[2].resolve({ ...statusResult, head: 'after' })
upstreams[0].resolve({ hasUpstream: true, upstreamName: 'before/main', ahead: 0, behind: 0 })
upstreams[1].resolve({ hasUpstream: true, upstreamName: 'during/main', ahead: 0, behind: 0 })
upstreams[2].resolve({ hasUpstream: true, upstreamName: 'after/main', ahead: 0, behind: 0 })
await Promise.all([...before, ...during, ...after])
expect(owner.getSnapshot(query())).toMatchObject({
revision: 2,
repositoryIdentity: { head: 'after' },
upstream: { upstreamName: 'after/main' },
freshness: {
status: { state: 'fresh', generation: 2 },
upstream: { state: 'fresh', generation: 2 }
}
})
})
it('isolates every host, path, status option, and upstream target dimension', async () => {
const owner = new GitRepositorySnapshotOwner()
const statusLoad = vi.fn(async () => statusResult)
const upstreamLoad = vi.fn(async () => ({ hasUpstream: false, ahead: 0, behind: 0 }))
const statusCases: [GitRepositoryExecutionIdentity, string, GitRepositoryStatusIdentity][] = [
[native, '/repo-a', defaultStatusIdentity],
[native, '/repo-b', defaultStatusIdentity],
[{ kind: 'wsl', distro: 'Ubuntu' }, '/repo-a', defaultStatusIdentity],
[{ kind: 'wsl', distro: 'Debian' }, '/repo-a', defaultStatusIdentity],
[native, '/repo-a', { ...defaultStatusIdentity, includeIgnored: true }],
[native, '/repo-a', { ...defaultStatusIdentity, reuseLineStats: true }],
[native, '/repo-a', { ...defaultStatusIdentity, bypassEffectiveUpstreamNegativeCache: true }],
[native, '/repo-a', { ...defaultStatusIdentity, limit: 20 }],
[native, '/repo-a', { ...defaultStatusIdentity, sharedLinkPaths: ['node_modules'] }]
]
const target = { remoteName: 'fork', branchName: 'feature' }
const targets: (GitPushTarget | undefined)[] = [
undefined,
target,
{ ...target, remoteName: 'origin' },
{ ...target, branchName: 'other' },
{ ...target, remoteUrl: 'https://github.com/example/fork.git' },
{ ...target, remoteCreated: false },
{ ...target, remoteCreated: true }
]
await Promise.all(
statusCases.map(([host, path, identity]) =>
owner.readStatus(host, path, identity, undefined, statusLoad)
)
)
await Promise.all(
targets.map((candidate) => owner.readUpstream(native, '/repo-a', candidate, upstreamLoad))
)
expect(statusLoad).toHaveBeenCalledTimes(statusCases.length)
expect(upstreamLoad).toHaveBeenCalledTimes(targets.length)
})
it('bounds repositories and projection variants without merging evicted identities', async () => {
const owner = new GitRepositorySnapshotOwner(2, 2)
const evictedPending = deferred<GitStatusResult>()
const evicted = owner.readStatus(
native,
'/repo-a',
defaultStatusIdentity,
undefined,
() => evictedPending.promise
)
await owner.readStatus(native, '/repo-b', defaultStatusIdentity, undefined, async () => ({
...statusResult,
head: 'repo-b'
}))
await owner.readStatus(native, '/repo-c', defaultStatusIdentity, undefined, async () => ({
...statusResult,
head: 'repo-c'
}))
evictedPending.resolve({ ...statusResult, head: 'repo-a-late' })
await evicted
expect(owner.getRetentionState().repositories).toBe(2)
expect(owner.getSnapshot(query(native, '/repo-a'))).toBeNull()
expect(owner.getSnapshot(query(native, '/repo-c'))?.repositoryIdentity.head).toBe('repo-c')
await owner.readStatus(
native,
'/repo-c',
{ ...defaultStatusIdentity, includeIgnored: true },
undefined,
async () => statusResult
)
await owner.readStatus(
native,
'/repo-c',
{ ...defaultStatusIdentity, reuseLineStats: true },
undefined,
async () => statusResult
)
const target = { remoteName: 'fork', branchName: 'feature' }
await owner.readUpstream(native, '/repo-c', undefined, async () => ({
hasUpstream: false,
ahead: 0,
behind: 0
}))
await owner.readUpstream(native, '/repo-c', target, async () => ({
hasUpstream: false,
ahead: 0,
behind: 0
}))
await owner.readUpstream(native, '/repo-c', { ...target, branchName: 'other' }, async () => ({
hasUpstream: false,
ahead: 0,
behind: 0
}))
expect(owner.getRetentionState()).toEqual({
repositories: 2,
statusProjections: 3,
upstreamProjections: 2
})
})
it('bounds retained status payloads without changing the caller result', async () => {
const owner = new GitRepositorySnapshotOwner()
const entries = Array.from(
{ length: MAX_GIT_REPOSITORY_SNAPSHOT_STATUS_ENTRIES + 1 },
(_, index) => ({
path: `generated/file-${index}.ts`,
status: 'untracked' as const,
area: 'untracked' as const
})
)
const result = { entries, conflictOperation: 'unknown' as const }
const returned = await owner.readStatus(
native,
'/large-repo',
{ ...defaultStatusIdentity, limit: 0 },
undefined,
async () => result
)
const snapshot = owner.getSnapshot(
query(native, '/large-repo', { ...defaultStatusIdentity, limit: 0 })
)
expect(returned.entries).toHaveLength(MAX_GIT_REPOSITORY_SNAPSHOT_STATUS_ENTRIES + 1)
expect(snapshot?.status.entries).toHaveLength(MAX_GIT_REPOSITORY_SNAPSHOT_STATUS_ENTRIES)
expect(snapshot?.status.retentionTruncated).toBe(true)
})
})
@@ -0,0 +1,319 @@
import type { GitPushTarget, GitStatusResult, GitUpstreamStatus } from '../../shared/types'
import { InFlightPromiseDedupe, stableInFlightKey } from '../../shared/in-flight-promise-dedupe'
import { GitStatusReadLeaseOwner } from './git-status-read-lease-owner'
import {
createGitRepositoryProjectionFreshness,
EMPTY_GIT_IGNORED_PATHS,
EMPTY_GIT_STATUS_ENTRIES,
freezeGitRepositoryStatus,
freezeGitRepositoryUpstream,
gitRepositoryScopeKey,
gitRepositoryStatusKey,
gitRepositoryUpstreamKey,
type GitRepositoryExecutionIdentity,
type GitRepositorySnapshot,
type GitRepositorySnapshotQuery,
type GitRepositoryStatusIdentity,
type GitRepositoryStatusProjection
} from './git-repository-snapshot-projection'
export type {
GitRepositoryExecutionIdentity,
GitRepositoryProjectionFreshness,
GitRepositorySnapshot,
GitRepositorySnapshotQuery,
GitRepositoryStatusIdentity
} from './git-repository-snapshot-projection'
export const MAX_GIT_REPOSITORY_SNAPSHOTS = 64
export const MAX_GIT_REPOSITORY_PROJECTION_VARIANTS = 4
type ProjectionRecord<T> = {
generation: number
generatedAt: number
revision: number
value: T
failedGeneration?: number
}
type RepositoryRecord = {
status: Map<string, ProjectionRecord<GitRepositoryStatusProjection>>
upstream: Map<string, ProjectionRecord<Readonly<GitUpstreamStatus>>>
}
function clampBound(value: number, maximum: number): number {
return Number.isFinite(value) ? Math.min(maximum, Math.max(1, Math.floor(value))) : maximum
}
export class GitRepositorySnapshotOwner {
private readonly statusReads = new GitStatusReadLeaseOwner<GitStatusResult>()
private readonly upstreamReads = new InFlightPromiseDedupe<GitUpstreamStatus>()
private readonly repositories = new Map<string, RepositoryRecord>()
private readonly maxRepositories: number
private readonly maxProjectionVariants: number
private generation = 0
private revision = 0
constructor(
maxRepositories = MAX_GIT_REPOSITORY_SNAPSHOTS,
maxProjectionVariants = MAX_GIT_REPOSITORY_PROJECTION_VARIANTS,
private readonly now: () => number = Date.now
) {
this.maxRepositories = clampBound(maxRepositories, MAX_GIT_REPOSITORY_SNAPSHOTS)
this.maxProjectionVariants = clampBound(
maxProjectionVariants,
MAX_GIT_REPOSITORY_PROJECTION_VARIANTS
)
}
readStatus(
executionIdentity: GitRepositoryExecutionIdentity,
worktreePath: string,
identity: GitRepositoryStatusIdentity,
signal: AbortSignal | undefined,
load: (sharedSignal: AbortSignal) => Promise<GitStatusResult>
): Promise<GitStatusResult> {
const repositoryKey = gitRepositoryScopeKey(executionIdentity, worktreePath)
const projectionKey = gitRepositoryStatusKey(identity)
const generation = this.generation
return this.statusReads.lease(
stableInFlightKey([repositoryKey, projectionKey]),
signal,
async (sharedSignal) => {
const repository = this.getOrCreateRepository(repositoryKey)
try {
const result = await load(sharedSignal)
this.publishStatus(repositoryKey, repository, projectionKey, generation, result)
return result
} catch (error) {
const retained = repository.status.get(projectionKey)
if (
!sharedSignal.aborted &&
generation === this.generation &&
this.repositories.get(repositoryKey) === repository &&
retained
) {
retained.failedGeneration = generation
}
this.removeEmptyRepository(repositoryKey, repository)
throw error
}
}
)
}
readUpstream(
executionIdentity: GitRepositoryExecutionIdentity,
worktreePath: string,
pushTarget: GitPushTarget | undefined,
load: () => Promise<GitUpstreamStatus>
): Promise<GitUpstreamStatus> {
const repositoryKey = gitRepositoryScopeKey(executionIdentity, worktreePath)
const projectionKey = gitRepositoryUpstreamKey(pushTarget)
const generation = this.generation
return this.upstreamReads.run(stableInFlightKey([repositoryKey, projectionKey]), async () => {
const repository = this.getOrCreateRepository(repositoryKey)
try {
const result = await load()
this.publishUpstream(repositoryKey, repository, projectionKey, generation, result)
return result
} catch (error) {
const retained = repository.upstream.get(projectionKey)
if (
generation === this.generation &&
this.repositories.get(repositoryKey) === repository &&
retained
) {
retained.failedGeneration = generation
}
this.removeEmptyRepository(repositoryKey, repository)
throw error
}
})
}
getSnapshot(query: GitRepositorySnapshotQuery): GitRepositorySnapshot | null {
const repositoryKey = gitRepositoryScopeKey(query.executionIdentity, query.worktreePath)
const repository = this.repositories.get(repositoryKey)
if (!repository) {
return null
}
this.touchRepository(repositoryKey, repository)
const statusIdentity = gitRepositoryStatusKey(query.statusIdentity)
const upstreamIdentity = gitRepositoryUpstreamKey(query.pushTarget)
const statusRecord = repository.status.get(statusIdentity)
const upstreamRecord = repository.upstream.get(upstreamIdentity)
if (!statusRecord && !upstreamRecord) {
return null
}
const statusFreshness = createGitRepositoryProjectionFreshness(
statusRecord,
this.generation,
statusIdentity
)
const embeddedUpstreamRecord =
query.pushTarget === undefined && statusRecord?.value.upstream ? statusRecord : undefined
const useEmbeddedUpstream =
embeddedUpstreamRecord &&
(!upstreamRecord || embeddedUpstreamRecord.revision > upstreamRecord.revision)
const selectedUpstreamRecord = useEmbeddedUpstream ? embeddedUpstreamRecord : upstreamRecord
const upstreamFreshness = selectedUpstreamRecord
? createGitRepositoryProjectionFreshness(
selectedUpstreamRecord,
this.generation,
useEmbeddedUpstream ? `status:${statusIdentity}` : upstreamIdentity
)
: createGitRepositoryProjectionFreshness(undefined, this.generation, upstreamIdentity)
const revision = Math.max(statusRecord?.revision ?? 0, upstreamRecord?.revision ?? 0)
const generatedAt = Math.max(statusRecord?.generatedAt ?? 0, upstreamRecord?.generatedAt ?? 0)
const status = statusRecord?.value
return Object.freeze({
revision,
generatedAt,
repositoryIdentity: status?.repositoryIdentity ?? Object.freeze({ head: null, branch: null }),
status:
status?.status ??
Object.freeze({
entries: EMPTY_GIT_STATUS_ENTRIES,
didHitLimit: false,
statusLength: null,
ignoredPaths: EMPTY_GIT_IGNORED_PATHS,
lineStatsState: 'missing' as const,
retentionTruncated: false
}),
upstream: useEmbeddedUpstream
? embeddedUpstreamRecord.value.upstream
: (upstreamRecord?.value ?? null),
conflicts: status?.conflicts ?? null,
worktreeGraphVersion: 0,
freshness: Object.freeze({
repositoryIdentity: statusFreshness,
status: statusFreshness,
upstream: upstreamFreshness,
conflicts: statusFreshness,
worktreeGraph: Object.freeze({
state: 'placeholder',
generation: this.generation,
currentGeneration: this.generation,
revision: null,
identity: null
})
})
})
}
invalidate(): void {
this.generation += 1
this.statusReads.invalidate()
this.upstreamReads.clear()
}
getRetentionState(): Readonly<{
repositories: number
statusProjections: number
upstreamProjections: number
}> {
let statusProjections = 0
let upstreamProjections = 0
for (const repository of this.repositories.values()) {
statusProjections += repository.status.size
upstreamProjections += repository.upstream.size
}
return Object.freeze({
repositories: this.repositories.size,
statusProjections,
upstreamProjections
})
}
private getOrCreateRepository(repositoryKey: string): RepositoryRecord {
const existing = this.repositories.get(repositoryKey)
if (existing) {
this.touchRepository(repositoryKey, existing)
return existing
}
const repository = { status: new Map(), upstream: new Map() }
this.repositories.set(repositoryKey, repository)
while (this.repositories.size > this.maxRepositories) {
const oldestKey = this.repositories.keys().next().value
if (oldestKey === undefined) {
break
}
this.repositories.delete(oldestKey)
}
return repository
}
private touchRepository(repositoryKey: string, repository: RepositoryRecord): void {
this.repositories.delete(repositoryKey)
this.repositories.set(repositoryKey, repository)
}
private removeEmptyRepository(repositoryKey: string, repository: RepositoryRecord): void {
if (
this.repositories.get(repositoryKey) === repository &&
repository.status.size === 0 &&
repository.upstream.size === 0
) {
this.repositories.delete(repositoryKey)
}
}
private publishStatus(
repositoryKey: string,
repository: RepositoryRecord,
projectionKey: string,
generation: number,
result: GitStatusResult
): void {
if (generation !== this.generation || this.repositories.get(repositoryKey) !== repository) {
return
}
const revision = this.nextRevision()
const generatedAt = this.now()
this.remember(repository.status, projectionKey, {
generation,
generatedAt,
revision,
value: freezeGitRepositoryStatus(result)
})
}
private publishUpstream(
repositoryKey: string,
repository: RepositoryRecord,
projectionKey: string,
generation: number,
result: GitUpstreamStatus
): void {
if (generation !== this.generation || this.repositories.get(repositoryKey) !== repository) {
return
}
this.remember(repository.upstream, projectionKey, {
generation,
generatedAt: this.now(),
revision: this.nextRevision(),
value: freezeGitRepositoryUpstream(result)
})
}
private remember<T>(projections: Map<string, T>, key: string, value: T): void {
projections.delete(key)
projections.set(key, value)
while (projections.size > this.maxProjectionVariants) {
const oldestKey = projections.keys().next().value
if (oldestKey === undefined) {
break
}
projections.delete(oldestKey)
}
}
private nextRevision(): number {
this.revision += 1
return this.revision
}
}
export const nativeAndWslGitRepositorySnapshotOwner = new GitRepositorySnapshotOwner()
@@ -0,0 +1,210 @@
import type {
GitConflictOperation,
GitPushTarget,
GitStatusEntry,
GitStatusResult,
GitUpstreamStatus
} from '../../shared/types'
import { stableInFlightKey } from '../../shared/in-flight-promise-dedupe'
export const MAX_GIT_REPOSITORY_SNAPSHOT_STATUS_ENTRIES = 1_000
export const MAX_GIT_REPOSITORY_SNAPSHOT_IGNORED_PATHS = 1_000
export const MAX_GIT_REPOSITORY_SNAPSHOT_TEXT_CODE_UNITS = 64 * 1_024
export type GitRepositoryExecutionIdentity =
| { kind: 'native' }
| { kind: 'wsl'; distro: string }
| { kind: 'ssh-provider'; connectionId: string }
export type GitRepositoryStatusIdentity = {
includeIgnored: boolean
reuseLineStats: boolean
bypassEffectiveUpstreamNegativeCache: boolean
limit: number | null
sharedLinkPaths: readonly string[]
}
export type GitRepositoryProjectionFreshness = Readonly<{
state: 'missing' | 'fresh' | 'stale' | 'failed' | 'placeholder'
generation: number
currentGeneration: number
revision: number | null
identity: string | null
}>
export type GitRepositorySnapshot = Readonly<{
revision: number
generatedAt: number
repositoryIdentity: Readonly<{ head: string | null; branch: string | null }>
status: Readonly<{
entries: readonly Readonly<GitStatusEntry>[]
didHitLimit: boolean
statusLength: number | null
ignoredPaths: readonly string[]
lineStatsState: 'missing' | 'complete' | 'skipped-at-limit'
retentionTruncated: boolean
}>
upstream: Readonly<GitUpstreamStatus> | null
conflicts: GitConflictOperation | null
worktreeGraphVersion: number
freshness: Readonly<{
repositoryIdentity: GitRepositoryProjectionFreshness
status: GitRepositoryProjectionFreshness
upstream: GitRepositoryProjectionFreshness
conflicts: GitRepositoryProjectionFreshness
worktreeGraph: GitRepositoryProjectionFreshness
}>
}>
export type GitRepositoryStatusProjection = {
repositoryIdentity: Readonly<{ head: string | null; branch: string | null }>
status: GitRepositorySnapshot['status']
upstream: Readonly<GitUpstreamStatus> | null
conflicts: GitConflictOperation
}
export type GitRepositorySnapshotQuery = {
executionIdentity: GitRepositoryExecutionIdentity
worktreePath: string
statusIdentity: GitRepositoryStatusIdentity
pushTarget?: GitPushTarget
}
export const EMPTY_GIT_STATUS_ENTRIES = Object.freeze([]) as readonly Readonly<GitStatusEntry>[]
export const EMPTY_GIT_IGNORED_PATHS = Object.freeze([]) as readonly string[]
export function gitRepositoryScopeKey(
executionIdentity: GitRepositoryExecutionIdentity,
worktreePath: string
): string {
return stableInFlightKey([executionIdentity, worktreePath])
}
export function gitRepositoryStatusKey(identity: GitRepositoryStatusIdentity): string {
return stableInFlightKey([
identity.includeIgnored,
identity.reuseLineStats,
identity.bypassEffectiveUpstreamNegativeCache,
identity.limit,
identity.sharedLinkPaths
])
}
export function gitRepositoryUpstreamKey(pushTarget: GitPushTarget | undefined): string {
return stableInFlightKey([
pushTarget
? [
'explicit-target',
pushTarget.remoteName,
pushTarget.branchName,
pushTarget.remoteUrl ?? null,
pushTarget.remoteCreated ?? null
]
: ['configured-upstream']
])
}
export function freezeGitRepositoryUpstream(
status: GitUpstreamStatus
): Readonly<GitUpstreamStatus> {
return Object.freeze({ ...status })
}
function copyBoundedGitStatusEntries(entries: GitStatusEntry[]): {
entries: readonly Readonly<GitStatusEntry>[]
textCodeUnits: number
} {
const retained: Readonly<GitStatusEntry>[] = []
let textCodeUnits = 0
for (const entry of entries) {
const entryCodeUnits = entry.path.length + (entry.oldPath?.length ?? 0)
if (
retained.length >= MAX_GIT_REPOSITORY_SNAPSHOT_STATUS_ENTRIES ||
textCodeUnits + entryCodeUnits > MAX_GIT_REPOSITORY_SNAPSHOT_TEXT_CODE_UNITS
) {
break
}
retained.push(
Object.freeze({
...entry,
...(entry.submodule ? { submodule: Object.freeze({ ...entry.submodule }) } : {})
})
)
textCodeUnits += entryCodeUnits
}
return { entries: Object.freeze(retained), textCodeUnits }
}
function copyBoundedGitIgnoredPaths(
ignoredPaths: string[],
textCodeUnits: number
): readonly string[] {
const retained: string[] = []
let retainedCodeUnits = textCodeUnits
for (const ignoredPath of ignoredPaths) {
if (
retained.length >= MAX_GIT_REPOSITORY_SNAPSHOT_IGNORED_PATHS ||
retainedCodeUnits + ignoredPath.length > MAX_GIT_REPOSITORY_SNAPSHOT_TEXT_CODE_UNITS
) {
break
}
retained.push(ignoredPath)
retainedCodeUnits += ignoredPath.length
}
return Object.freeze(retained)
}
export function freezeGitRepositoryStatus(result: GitStatusResult): GitRepositoryStatusProjection {
const boundedEntries = copyBoundedGitStatusEntries(result.entries)
const ignoredPaths = copyBoundedGitIgnoredPaths(
result.ignoredPaths ?? [],
boundedEntries.textCodeUnits
)
const retentionTruncated =
boundedEntries.entries.length !== result.entries.length ||
ignoredPaths.length !== (result.ignoredPaths?.length ?? 0)
return Object.freeze({
repositoryIdentity: Object.freeze({
head: result.head ?? null,
branch: result.branch ?? null
}),
status: Object.freeze({
entries: boundedEntries.entries,
didHitLimit: result.didHitLimit === true,
statusLength: result.statusLength ?? null,
ignoredPaths,
lineStatsState: result.didHitLimit === true ? 'skipped-at-limit' : 'complete',
retentionTruncated
}),
upstream: result.upstreamStatus ? freezeGitRepositoryUpstream(result.upstreamStatus) : null,
conflicts: result.conflictOperation
})
}
export function createGitRepositoryProjectionFreshness(
record:
| {
generation: number
revision: number
failedGeneration?: number
}
| undefined,
generation: number,
identity: string
): GitRepositoryProjectionFreshness {
const state =
record?.failedGeneration === generation
? 'failed'
: record
? record.generation === generation
? 'fresh'
: 'stale'
: 'missing'
return Object.freeze({
state,
generation: record?.generation ?? generation,
currentGeneration: generation,
revision: record?.revision ?? null,
identity
})
}
@@ -1,120 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { GitPushTarget, GitUpstreamStatus } from '../../shared/types'
import {
GitUpstreamStatusReadOwner,
type GitUpstreamStatusExecutionIdentity
} from './git-upstream-status-read-owner'
const upstreamStatus: GitUpstreamStatus = {
hasUpstream: true,
upstreamName: 'origin/main',
ahead: 1,
behind: 0
}
function deferredPromise<T>(): {
promise: Promise<T>
resolve: (value: T) => void
reject: (error: unknown) => void
} {
let resolve!: (value: T) => void
let reject!: (error: unknown) => void
const promise = new Promise<T>((innerResolve, innerReject) => {
resolve = innerResolve
reject = innerReject
})
return { promise, resolve, reject }
}
describe('GitUpstreamStatusReadOwner', () => {
it('shares one live read across ten identical callers', async () => {
const owner = new GitUpstreamStatusReadOwner()
const pending = deferredPromise<GitUpstreamStatus>()
const load = vi.fn(() => pending.promise)
const reads = Array.from({ length: 10 }, () =>
owner.read({ kind: 'native' }, '/repo', undefined, load)
)
await Promise.resolve()
expect(load).toHaveBeenCalledTimes(1)
pending.resolve(upstreamStatus)
await expect(Promise.all(reads)).resolves.toEqual(
Array.from({ length: 10 }, () => upstreamStatus)
)
})
it('isolates paths, execution hosts, and every explicit target field', async () => {
const owner = new GitUpstreamStatusReadOwner()
const load = vi.fn().mockResolvedValue(upstreamStatus)
const baseTarget: GitPushTarget = {
remoteName: 'fork',
branchName: 'feature'
}
const cases: [GitUpstreamStatusExecutionIdentity, string, GitPushTarget | undefined][] = [
[{ kind: 'native' }, '/repo-a', undefined],
[{ kind: 'native' }, '/repo-b', undefined],
[{ kind: 'wsl', distro: 'Ubuntu' }, '/repo-a', undefined],
[{ kind: 'wsl', distro: 'Debian' }, '/repo-a', undefined],
[{ kind: 'native' }, '/repo-a', baseTarget],
[{ kind: 'native' }, '/repo-a', { ...baseTarget, remoteName: 'origin' }],
[{ kind: 'native' }, '/repo-a', { ...baseTarget, branchName: 'other' }],
[
{ kind: 'native' },
'/repo-a',
{ ...baseTarget, remoteUrl: 'https://github.com/example/fork.git' }
],
[{ kind: 'native' }, '/repo-a', { ...baseTarget, remoteCreated: false }],
[{ kind: 'native' }, '/repo-a', { ...baseTarget, remoteCreated: true }],
[{ kind: 'ssh-provider' }, '/repo-a', baseTarget]
]
await Promise.all(
cases.map(([identity, worktreePath, target]) =>
owner.read(identity, worktreePath, target, load)
)
)
expect(load).toHaveBeenCalledTimes(cases.length)
})
it('runs fresh work after success and rejection', async () => {
const owner = new GitUpstreamStatusReadOwner()
const failure = new Error('upstream failed')
const load = vi
.fn<() => Promise<GitUpstreamStatus>>()
.mockResolvedValueOnce(upstreamStatus)
.mockRejectedValueOnce(failure)
.mockResolvedValueOnce(upstreamStatus)
await expect(owner.read({ kind: 'native' }, '/repo', undefined, load)).resolves.toBe(
upstreamStatus
)
await expect(owner.read({ kind: 'native' }, '/repo', undefined, load)).rejects.toBe(failure)
await expect(owner.read({ kind: 'native' }, '/repo', undefined, load)).resolves.toBe(
upstreamStatus
)
expect(load).toHaveBeenCalledTimes(3)
})
it('fences before, during, and after mutation generations', async () => {
const owner = new GitUpstreamStatusReadOwner()
const pendingReads = Array.from({ length: 3 }, () => deferredPromise<GitUpstreamStatus>())
const load = vi.fn(() => pendingReads[load.mock.calls.length - 1].promise)
const before = owner.read({ kind: 'native' }, '/repo', undefined, load)
await Promise.resolve()
owner.invalidate()
const during = owner.read({ kind: 'native' }, '/repo', undefined, load)
await Promise.resolve()
owner.invalidate()
const after = owner.read({ kind: 'native' }, '/repo', undefined, load)
await Promise.resolve()
pendingReads.forEach((pending) => pending.resolve(upstreamStatus))
await expect(Promise.all([before, during, after])).resolves.toEqual(
Array.from({ length: 3 }, () => upstreamStatus)
)
expect(load).toHaveBeenCalledTimes(3)
})
})
@@ -1,39 +0,0 @@
import type { GitPushTarget, GitUpstreamStatus } from '../../shared/types'
import { InFlightPromiseDedupe, stableInFlightKey } from '../../shared/in-flight-promise-dedupe'
export type GitUpstreamStatusExecutionIdentity =
| { kind: 'native' }
| { kind: 'wsl'; distro: string }
| { kind: 'ssh-provider' }
export class GitUpstreamStatusReadOwner {
private readonly inFlightReads = new InFlightPromiseDedupe<GitUpstreamStatus>()
read(
executionIdentity: GitUpstreamStatusExecutionIdentity,
worktreePath: string,
pushTarget: GitPushTarget | undefined,
load: () => Promise<GitUpstreamStatus>
): Promise<GitUpstreamStatus> {
const key = stableInFlightKey([
executionIdentity,
worktreePath,
pushTarget
? [
'explicit-target',
pushTarget.remoteName,
pushTarget.branchName,
pushTarget.remoteUrl ?? null,
pushTarget.remoteCreated ?? null
]
: ['configured-upstream']
])
return this.inFlightReads.run(key, load)
}
invalidate(): void {
this.inFlightReads.clear()
}
}
export const nativeAndWslGitUpstreamStatusReadOwner = new GitUpstreamStatusReadOwner()
+33
View File
@@ -108,6 +108,7 @@ import {
getBranchCompare,
getCommitCompare,
getDiff,
getGitRepositorySnapshot,
getStagedCommitContext,
getStatus,
getSubmoduleStatus,
@@ -1176,6 +1177,38 @@ describe('getStatus', () => {
)
})
it('publishes the live native status result into the repository snapshot owner', async () => {
gitExecFileAsyncMock.mockResolvedValueOnce({
stdout:
'# branch.oid abcdef1234567890\n' +
'# branch.head feature/snapshot\n' +
'# branch.upstream origin/feature/snapshot\n' +
'# branch.ab +2 -1\n' +
'1 .M N... 100644 100644 100644 aaaa aaaa src/app.ts\n'
})
const result = await getStatus('/snapshot-repo')
const snapshot = getGitRepositorySnapshot('/snapshot-repo')
expect(snapshot).toMatchObject({
repositoryIdentity: {
head: 'abcdef1234567890',
branch: 'refs/heads/feature/snapshot'
},
status: {
entries: result.entries,
didHitLimit: false,
lineStatsState: 'complete'
},
upstream: result.upstreamStatus,
conflicts: 'unknown',
freshness: {
status: { state: 'fresh' },
upstream: { state: 'fresh' }
}
})
})
it('coalesces identical in-flight status reads without caching after settle', async () => {
readFileMock.mockResolvedValue('gitdir: /repo/.git/worktrees/feature\n')
existsSyncMock.mockReturnValue(false)
+37 -20
View File
@@ -12,6 +12,7 @@ import type {
GitConflictOperation,
GitDiffResult,
GitFileStatus,
GitPushTarget,
GitStatusEntry,
GitStatusResult,
GitUpstreamStatus
@@ -52,9 +53,12 @@ import { getLargeDiffRenderLimit } from '../../shared/large-diff-render-limit'
import { InFlightPromiseDedupe, stableInFlightKey } from '../../shared/in-flight-promise-dedupe'
import type { GitRuntimeOptions } from './git-runtime-options'
import { gitOptionsForWorktree } from './git-runtime-options'
import { GitStatusReadLeaseOwner } from './git-status-read-lease-owner'
import { parseGitRevListFirstParentOid } from '../../shared/git-rev-list-output'
import { nativeAndWslGitUpstreamStatusReadOwner } from './git-upstream-status-read-owner'
import {
nativeAndWslGitRepositorySnapshotOwner,
type GitRepositoryExecutionIdentity,
type GitRepositorySnapshot
} from './git-repository-snapshot-owner'
import {
beginGitStatusLineStatsCacheWrite,
clearGitStatusLineStatsCache,
@@ -94,13 +98,11 @@ const effectiveUpstreamStatusInFlight = new Map<string, Promise<GitUpstreamStatu
const retiredEffectiveUpstreamStatusInFlight = new Map<string, Promise<GitUpstreamStatus>>()
const gitDiffReadDedupe = new InFlightPromiseDedupe<GitDiffResult>()
const effectiveUpstreamStatusWriteGeneration = new Map<string, number>()
const statusReadLeaseOwner = new GitStatusReadLeaseOwner<GitStatusResult>()
// Why: clear both diff and status in-flight caches; clearing only diff would let getStatus() join a pre-mutation read.
export function invalidateGitReadCaches(): void {
gitDiffReadDedupe.clear()
statusReadLeaseOwner.invalidate()
nativeAndWslGitUpstreamStatusReadOwner.invalidate()
nativeAndWslGitRepositorySnapshotOwner.invalidate()
clearGitStatusLineStatsCache()
clearSubmodulePathsCache()
resolvedUpstreamNameCache.clear()
@@ -219,26 +221,41 @@ export async function getStatus(
options: GetStatusOptions = {}
): Promise<GitStatusResult> {
gitDiffReadDedupe.clear()
// Why: dedupe only concurrent identical reads; after settle, callers must run a fresh read.
const cacheKey = getStatusReadKey(worktreePath, options)
return statusReadLeaseOwner.lease(cacheKey, options.signal, (sharedSignal) =>
runGetStatus(worktreePath, { ...options, signal: sharedSignal })
return nativeAndWslGitRepositorySnapshotOwner.readStatus(
getStatusExecutionIdentity(options),
worktreePath,
getStatusSnapshotIdentity(options),
options.signal,
(sharedSignal) => runGetStatus(worktreePath, { ...options, signal: sharedSignal })
)
}
function getStatusReadKey(worktreePath: string, options: GetStatusOptions): string {
// Why: each key part can change the output shape or runtime routing.
const limit = resolveGitStatusLimit(options.limit)
return stableInFlightKey([
export function getGitRepositorySnapshot(
worktreePath: string,
options: GetStatusOptions = {},
pushTarget?: GitPushTarget
): GitRepositorySnapshot | null {
return nativeAndWslGitRepositorySnapshotOwner.getSnapshot({
executionIdentity: getStatusExecutionIdentity(options),
worktreePath,
options.wslDistro ?? '',
options.includeIgnored === true,
options.reuseLineStats === true,
options.bypassEffectiveUpstreamNegativeCache === true,
limit,
statusIdentity: getStatusSnapshotIdentity(options),
pushTarget
})
}
function getStatusExecutionIdentity(options: GetStatusOptions): GitRepositoryExecutionIdentity {
return options.wslDistro ? { kind: 'wsl', distro: options.wslDistro } : { kind: 'native' }
}
function getStatusSnapshotIdentity(options: GetStatusOptions) {
return {
includeIgnored: options.includeIgnored === true,
reuseLineStats: options.reuseLineStats === true,
bypassEffectiveUpstreamNegativeCache: options.bypassEffectiveUpstreamNegativeCache === true,
limit: resolveGitStatusLimit(options.limit),
// Why: this changes which entries survive, so it must not share a cache slot.
options.sharedLinkPaths ?? []
])
sharedLinkPaths: options.sharedLinkPaths ?? []
}
}
/** Remove untracked entries that are shared symlinks Orca created.
+19 -1
View File
@@ -11,7 +11,7 @@ vi.mock('./runner', () => ({
}))
import { getUpstreamStatus, invalidateGitUpstreamStatusReads } from './upstream'
import { runWithGitReadCacheInvalidation } from './status'
import { getGitRepositorySnapshot, runWithGitReadCacheInvalidation } from './status'
const missingTrackingRefError = new Error(
"fatal: ambiguous argument 'HEAD@{u}': unknown revision or path not in the working tree.\n" +
@@ -71,6 +71,24 @@ describe('getUpstreamStatus', () => {
)
})
it('publishes the live native upstream result into the repository snapshot owner', async () => {
gitExecFileAsyncMock
.mockResolvedValueOnce({ stdout: 'main\n' })
.mockResolvedValueOnce({ stdout: 'origin/main\n' })
.mockResolvedValueOnce({ stdout: '2\t0\n' })
const result = await getUpstreamStatus('/snapshot-upstream-repo')
const snapshot = getGitRepositorySnapshot('/snapshot-upstream-repo')
expect(snapshot).toMatchObject({
upstream: result,
freshness: {
status: { state: 'missing' },
upstream: { state: 'fresh' }
}
})
})
it('isolates physical reads by worktree, native or WSL host, and every target field', async () => {
gitExecFileAsyncMock.mockImplementation((args: string[]) => {
if (args[0] === 'symbolic-ref') {
+3 -3
View File
@@ -5,14 +5,14 @@ import { getEffectiveGitUpstreamStatus } from '../../shared/git-effective-upstre
import { getPublishTargetStatus } from '../../shared/git-publish-target-status'
import { gitExecFileAsync } from './runner'
import { validateGitPushTarget } from './push-target-validation'
import { nativeAndWslGitUpstreamStatusReadOwner } from './git-upstream-status-read-owner'
import { nativeAndWslGitRepositorySnapshotOwner } from './git-repository-snapshot-owner'
type GitExecOptions = {
wslDistro?: string
}
export function invalidateGitUpstreamStatusReads(): void {
nativeAndWslGitUpstreamStatusReadOwner.invalidate()
nativeAndWslGitRepositorySnapshotOwner.invalidate()
}
function gitExecOptions(
@@ -85,7 +85,7 @@ export function getUpstreamStatus(
const executionIdentity = options.wslDistro
? ({ kind: 'wsl', distro: options.wslDistro } as const)
: ({ kind: 'native' } as const)
return nativeAndWslGitUpstreamStatusReadOwner.read(
return nativeAndWslGitRepositorySnapshotOwner.readUpstream(
executionIdentity,
worktreePath,
pushTarget,
@@ -1121,6 +1121,75 @@ describe('SshGitProvider', () => {
expect(mux.request).toHaveBeenCalledTimes(3)
})
it('publishes combined immutable snapshots per provider incarnation', async () => {
mux.request.mockImplementation((method) => {
if (method === 'git.status') {
return Promise.resolve({
entries: [{ path: 'src/app.ts', status: 'modified', area: 'unstaged' }],
conflictOperation: 'rebase',
head: 'abc123',
branch: 'refs/heads/feature'
})
}
if (method === 'git.upstreamStatus') {
return Promise.resolve({
hasUpstream: true,
upstreamName: 'origin/feature',
ahead: 1,
behind: 0
})
}
return Promise.resolve(undefined)
})
await provider.getStatus('/home/user/repo')
await provider.getUpstreamStatus('/home/user/repo')
const snapshot = provider.getRepositorySnapshot('/home/user/repo')
const replacement = new SshGitProvider('conn-1', mux as never)
expect(snapshot).toMatchObject({
repositoryIdentity: { head: 'abc123', branch: 'refs/heads/feature' },
status: { entries: [{ path: 'src/app.ts' }] },
upstream: { upstreamName: 'origin/feature' },
conflicts: 'rebase',
freshness: {
status: { state: 'fresh' },
upstream: { state: 'fresh' }
}
})
expect(Object.isFrozen(snapshot)).toBe(true)
expect(replacement.getRepositorySnapshot('/home/user/repo')).toBeNull()
})
it('marks retained snapshots stale across the existing SSH mutation fence', async () => {
mux.request.mockImplementation((method) => {
if (method === 'git.status') {
return Promise.resolve({
entries: [],
conflictOperation: 'unknown',
head: 'abc123',
branch: 'refs/heads/main'
})
}
return Promise.resolve(undefined)
})
await provider.getStatus('/home/user/repo')
const before = provider.getRepositorySnapshot('/home/user/repo')
await provider.stageFile('/home/user/repo', 'src/app.ts')
const after = provider.getRepositorySnapshot('/home/user/repo')
expect(before?.freshness.status.state).toBe('fresh')
expect(after?.revision).toBe(before?.revision)
expect(after?.freshness.status).toMatchObject({
state: 'stale',
generation: before!.freshness.status.generation
})
expect(after!.freshness.status.currentGeneration).toBeGreaterThan(
after!.freshness.status.generation
)
})
it('getUpstreamStatus forwards an explicit push target', async () => {
const upstreamResult = { hasUpstream: true, upstreamName: 'fork/feature', ahead: 0, behind: 1 }
mux.request.mockResolvedValue(upstreamResult)
+44 -18
View File
@@ -32,8 +32,10 @@ import {
} from '../git/max-buffer-overflow'
import { InFlightPromiseDedupe, stableInFlightKey } from '../../shared/in-flight-promise-dedupe'
import { gitExecMutatesRepository } from '../../shared/git-exec-mutation'
import { GitStatusReadLeaseOwner } from '../git/git-status-read-lease-owner'
import { GitUpstreamStatusReadOwner } from '../git/git-upstream-status-read-owner'
import {
GitRepositorySnapshotOwner,
type GitRepositorySnapshot
} from '../git/git-repository-snapshot-owner'
type NonInteractiveExecQueueEntry = {
started: boolean
@@ -85,8 +87,7 @@ function filterUntrackedPorcelainStatus(stdout: string | undefined): string | un
export class SshGitProvider implements IGitProvider {
private readonly gitDiffReadDedupe = new InFlightPromiseDedupe<GitDiffResult | GitDiffResult[]>()
private readonly statusReadLeaseOwner = new GitStatusReadLeaseOwner<GitStatusResult>()
private readonly upstreamStatusReadOwner = new GitUpstreamStatusReadOwner()
private readonly repositorySnapshotOwner = new GitRepositorySnapshotOwner()
private connectionId: string
private mux: SshChannelMultiplexer
@@ -103,8 +104,7 @@ export class SshGitProvider implements IGitProvider {
private invalidateGitReads(): void {
this.gitDiffReadDedupe.clear()
this.statusReadLeaseOwner.invalidate()
this.upstreamStatusReadOwner.invalidate()
this.repositorySnapshotOwner.invalidate()
}
private loggedWorktreeIsCleanFallback = false
@@ -142,17 +142,23 @@ export class SshGitProvider implements IGitProvider {
...upstreamCacheBypassArgs,
...lineStatsReuseArgs
}
const key = stableInFlightKey([
return this.repositorySnapshotOwner.readStatus(
{ kind: 'ssh-provider', connectionId: this.connectionId },
worktreePath,
options?.includeIgnored === true,
options?.bypassEffectiveUpstreamNegativeCache === true,
options?.reuseLineStats === true
])
return this.statusReadLeaseOwner.lease(key, options?.signal, async (sharedSignal) => {
return (await this.mux.request('git.status', request, {
signal: sharedSignal
})) as GitStatusResult
})
{
includeIgnored: options?.includeIgnored === true,
reuseLineStats: options?.reuseLineStats === true,
bypassEffectiveUpstreamNegativeCache:
options?.bypassEffectiveUpstreamNegativeCache === true,
limit: null,
sharedLinkPaths: []
},
options?.signal,
async (sharedSignal) =>
(await this.mux.request('git.status', request, {
signal: sharedSignal
})) as GitStatusResult
)
}
async getSubmoduleStatus(
@@ -510,8 +516,8 @@ export class SshGitProvider implements IGitProvider {
worktreePath: string,
pushTarget?: GitPushTarget
): Promise<GitUpstreamStatus> {
return this.upstreamStatusReadOwner.read(
{ kind: 'ssh-provider' },
return this.repositorySnapshotOwner.readUpstream(
{ kind: 'ssh-provider', connectionId: this.connectionId },
worktreePath,
pushTarget,
async () =>
@@ -522,6 +528,26 @@ export class SshGitProvider implements IGitProvider {
)
}
getRepositorySnapshot(
worktreePath: string,
options?: GitProviderStatusOptions,
pushTarget?: GitPushTarget
): GitRepositorySnapshot | null {
return this.repositorySnapshotOwner.getSnapshot({
executionIdentity: { kind: 'ssh-provider', connectionId: this.connectionId },
worktreePath,
statusIdentity: {
includeIgnored: options?.includeIgnored === true,
reuseLineStats: options?.reuseLineStats === true,
bypassEffectiveUpstreamNegativeCache:
options?.bypassEffectiveUpstreamNegativeCache === true,
limit: null,
sharedLinkPaths: []
},
pushTarget
})
}
async pushBranch(
worktreePath: string,
publish = false,
@@ -0,0 +1,250 @@
import { Session } from 'node:inspector'
import { performance } from 'node:perf_hooks'
import type { GitStatusEntry, GitStatusResult } from '../../src/shared/types'
import { GitStatusReadLeaseOwner } from '../../src/main/git/git-status-read-lease-owner'
import {
GitRepositorySnapshotOwner,
type GitRepositoryStatusIdentity
} from '../../src/main/git/git-repository-snapshot-owner'
type Arm = 'lease-only' | 'snapshot-owner'
type Measurement = { cpuMicrosecondsPerRead: number; wallMicrosecondsPerRead: number }
type HeapProfileNode = { selfSize: number; children?: HeapProfileNode[] }
const native = { kind: 'native' } as const
const statusIdentity: GitRepositoryStatusIdentity = {
includeIgnored: false,
reuseLineStats: false,
bypassEffectiveUpstreamNegativeCache: false,
limit: 1_000,
sharedLinkPaths: []
}
const configurations = [
{ entries: 0, timingReads: 10_000, warmupReads: 2_000, allocationReads: 5_000 },
{ entries: 100, timingReads: 2_000, warmupReads: 400, allocationReads: 500 },
{ entries: 1_000, timingReads: 250, warmupReads: 50, allocationReads: 100 }
]
const timingRounds = 11
const allocationRounds = 7
function createStatusResult(entryCount: number): GitStatusResult {
const entries: GitStatusEntry[] = Array.from({ length: entryCount }, (_, index) => ({
path: `src/features/feature-${index}/index.ts`,
status: 'modified',
area: 'unstaged',
added: index % 7,
removed: index % 5
}))
return {
entries,
conflictOperation: 'unknown',
head: '0123456789abcdef',
branch: 'main',
upstreamStatus: {
hasUpstream: true,
upstreamName: 'origin/main',
ahead: 2,
behind: 3,
behindCommitsArePatchEquivalent: false
}
}
}
function createReader(arm: Arm, result: GitStatusResult): () => Promise<GitStatusResult> {
if (arm === 'lease-only') {
const owner = new GitStatusReadLeaseOwner<GitStatusResult>()
return () => owner.lease('native:/repo', undefined, async () => result)
}
const owner = new GitRepositorySnapshotOwner()
return () => owner.readStatus(native, '/repo', statusIdentity, undefined, async () => result)
}
async function runReads(read: () => Promise<GitStatusResult>, repetitions: number): Promise<void> {
for (let index = 0; index < repetitions; index += 1) {
await read()
}
}
function median(values: number[]): number {
const sorted = [...values].sort((left, right) => left - right)
return sorted[Math.floor(sorted.length / 2)]
}
async function measureTiming(
read: () => Promise<GitStatusResult>,
repetitions: number
): Promise<Measurement> {
const startedCpu = process.cpuUsage()
const startedWall = performance.now()
await runReads(read, repetitions)
const wallMilliseconds = performance.now() - startedWall
const cpu = process.cpuUsage(startedCpu)
return {
cpuMicrosecondsPerRead: (cpu.user + cpu.system) / repetitions,
wallMicrosecondsPerRead: (wallMilliseconds * 1_000) / repetitions
}
}
function post<T>(session: Session, method: string, parameters?: object): Promise<T> {
return new Promise((resolve, reject) => {
session.post(method, parameters, (error, result) => {
if (error) {
reject(error)
} else {
resolve(result as T)
}
})
})
}
function sumSampledBytes(node: HeapProfileNode): number {
return (
node.selfSize +
(node.children?.reduce((total, child) => total + sumSampledBytes(child), 0) ?? 0)
)
}
async function measureSampledAllocation(
read: () => Promise<GitStatusResult>,
repetitions: number
): Promise<number> {
globalThis.gc?.()
const session = new Session()
session.connect()
try {
await post(session, 'HeapProfiler.startSampling', {
samplingInterval: 128,
includeObjectsCollectedByMajorGC: true,
includeObjectsCollectedByMinorGC: true
})
await runReads(read, repetitions)
const result = await post<{ profile: { head: HeapProfileNode } }>(
session,
'HeapProfiler.stopSampling'
)
return sumSampledBytes(result.profile.head) / repetitions
} finally {
session.disconnect()
}
}
async function verifyCallerResult(result: GitStatusResult): Promise<object> {
const owner = new GitRepositorySnapshotOwner()
const before = JSON.stringify(result)
const returned = await owner.readStatus(
native,
'/repo',
statusIdentity,
undefined,
async () => result
)
const snapshot = owner.getSnapshot({
executionIdentity: native,
worktreePath: '/repo',
statusIdentity
})
return {
returnedOriginalReference: returned === result,
callerResultUnchanged: JSON.stringify(result) === before,
callerResultFrozen: Object.isFrozen(result),
callerEntriesFrozen: Object.isFrozen(result.entries),
snapshotFrozen: Object.isFrozen(snapshot),
snapshotEntriesFrozen: Object.isFrozen(snapshot?.status.entries),
snapshotEntryCopies: result.entries.every(
(entry, index) => snapshot?.status.entries[index] !== entry
)
}
}
async function main(): Promise<void> {
if (!globalThis.gc) {
throw new Error('Run with node --expose-gc --import tsx')
}
const results = []
for (const configuration of configurations) {
const statusResult = createStatusResult(configuration.entries)
const readers = {
'lease-only': createReader('lease-only', statusResult),
'snapshot-owner': createReader('snapshot-owner', statusResult)
}
await runReads(readers['lease-only'], configuration.warmupReads)
await runReads(readers['snapshot-owner'], configuration.warmupReads)
const timing: Record<Arm, Measurement[]> = {
'lease-only': [],
'snapshot-owner': []
}
for (let round = 0; round < timingRounds; round += 1) {
const order: Arm[] =
round % 2 === 0 ? ['lease-only', 'snapshot-owner'] : ['snapshot-owner', 'lease-only']
for (const arm of order) {
timing[arm].push(await measureTiming(readers[arm], configuration.timingReads))
}
}
const sampledAllocation: Record<Arm, number[]> = {
'lease-only': [],
'snapshot-owner': []
}
for (let round = 0; round < allocationRounds; round += 1) {
for (const arm of ['lease-only', 'snapshot-owner'] as const) {
sampledAllocation[arm].push(
await measureSampledAllocation(readers[arm], configuration.allocationReads)
)
}
}
const medianCpu = {
'lease-only': median(timing['lease-only'].map((value) => value.cpuMicrosecondsPerRead)),
'snapshot-owner': median(
timing['snapshot-owner'].map((value) => value.cpuMicrosecondsPerRead)
)
}
const medianWall = {
'lease-only': median(timing['lease-only'].map((value) => value.wallMicrosecondsPerRead)),
'snapshot-owner': median(
timing['snapshot-owner'].map((value) => value.wallMicrosecondsPerRead)
)
}
const medianAllocatedBytes = {
'lease-only': median(sampledAllocation['lease-only']),
'snapshot-owner': median(sampledAllocation['snapshot-owner'])
}
results.push({
entries: configuration.entries,
timingReadsPerRound: configuration.timingReads,
timingRounds,
allocationReadsPerRound: configuration.allocationReads,
allocationRounds,
medianCpuMicrosecondsPerRead: medianCpu,
incrementalCpuMicrosecondsPerRead: medianCpu['snapshot-owner'] - medianCpu['lease-only'],
medianWallMicrosecondsPerRead: medianWall,
incrementalWallMicrosecondsPerRead: medianWall['snapshot-owner'] - medianWall['lease-only'],
medianSampledAllocatedBytesPerRead: medianAllocatedBytes,
incrementalSampledAllocatedBytesPerRead:
medianAllocatedBytes['snapshot-owner'] - medianAllocatedBytes['lease-only'],
immutablePublicationOperations: {
entryCopies: configuration.entries,
objectFreezeCalls: configuration.entries + 6
},
callerVerification: await verifyCallerResult(statusResult)
})
}
console.log(
JSON.stringify(
{
node: process.version,
platform: `${process.platform}-${process.arch}`,
results
},
null,
2
)
)
}
void main().catch((error: unknown) => {
console.error(error)
process.exitCode = 1
})
@@ -0,0 +1,235 @@
# Phase 2 Git repository snapshot owner — 2026-07-30
## Decision
Retain the first live host-scoped, versioned Git repository snapshot owner. Native, exact-distro
WSL, and each `SshGitProvider` incarnation now route both status and upstream reads through this
owner while current APIs continue returning fresh post-settlement work. No TTL result reuse, Git
command, SSH RPC method, payload, renderer subscription, or new RPC was added.
Checkpoint: `b680c59da6d28767af9ae3cd5592ca8b6038db0d`.
## Live ownership
`GitRepositorySnapshotOwner` now owns the existing cancellation-safe status leases, upstream
in-flight sharing, mutation generations, and bounded snapshot publication. The native/WSL process
uses one host-keyed owner; each SSH provider constructs its own owner, and the key also includes
its connection id. A replacement provider therefore begins with no retained snapshot even when it
reuses the same connection id.
Status identities include:
- native versus the exact WSL distro or the per-instance SSH provider;
- worktree path;
- `includeIgnored`;
- `reuseLineStats`;
- effective-upstream negative-cache bypass;
- resolved local status limit; and
- ordered shared-link paths.
Upstream identities include the same execution host and worktree scope, configured-upstream versus
explicit-target mode, and every `GitPushTarget` field: `remoteName`, `branchName`, `remoteUrl`, and
absent versus explicit `remoteCreated: false`.
## Snapshot projection
Each immutable projection contains:
- monotonic process-owner revision and generation timestamp;
- repository head and branch identity;
- immutable status entries, ignored paths, limit state, status length, line-stat state, and
retention-truncation state;
- configured or explicit-target upstream status, including patch-equivalence fields;
- conflict operation;
- worktree graph version placeholder `0`; and
- per-projection `missing`, `fresh`, `stale`, `failed`, or `placeholder` state with publication
generation, current generation, revision, and exact projection identity.
A status result's embedded configured upstream can populate the combined snapshot without another
command. A later explicit upstream read updates only its exact target projection. For configured
upstream, the newest direct or status-embedded projection wins, so a fresh post-invalidation status
cannot be shadowed by a stale direct read.
The existing pre/post mutation wrappers increment the owner generation and clear only joinability.
Already-issued caller promises may settle, but their captured record identity and generation must
still match before they can publish. Reads begun before or during a mutation therefore cannot
repopulate the current snapshot after either fence.
## Retention bounds
The owner retains at most:
- 64 repository/worktree scopes;
- four status variants and four upstream variants per scope;
- 1,000 status entries and 1,000 ignored paths per status projection; and
- 64 Ki code units across retained status and ignored paths.
Current status callers still receive their complete original result. If snapshot retention trims a
limit-disabled or unusually large result, `retentionTruncated` is true. Repository and projection
maps use deterministic insertion eviction; publication also requires the exact retained record
object, so a late completion from an evicted identity cannot publish into a newly created record
with the same path.
## A/B physical counts
Opt-in deterministic benchmarks issue ten concurrent identical calls against the mocked physical
Git and mux boundaries.
| Boundary | A — checkpoint | B — snapshot owner |
| ---------------------------------------- | -------------: | -----------------: |
| Local status, unsignalled callers | 1 | 1 |
| Local status, distinct-signalled callers | 1 | 1 |
| Local configured-upstream Git calls | 4 | 4 |
| SSH status, unsignalled callers | 1 | 1 |
| SSH status, distinct-signalled callers | 1 | 1 |
| SSH upstream RPCs | 1 | 1 |
The exact local status command remains:
```text
git -c core.quotePath=false status --porcelain=v2 --branch --untracked-files=all
```
The configured-upstream chain remains:
```text
git symbolic-ref --quiet --short HEAD
git rev-parse --abbrev-ref HEAD@{u}
git rev-list --left-right --count HEAD...origin/main
git log --oneline --cherry-mark --right-only HEAD...origin/main --
```
SSH methods and default payloads remain exactly:
```json
[
{
"method": "git.status",
"payload": { "worktreePath": "/home/user/repo" }
},
{
"method": "git.upstreamStatus",
"payload": { "worktreePath": "/home/user/repo" }
}
]
```
True status options and explicit targets retain their existing fields; false or absent status
options and absent `pushTarget` remain omitted. Baseline artifacts are
`/tmp/git-snapshot-status-a.json`, `/tmp/git-snapshot-upstream-a.json`,
`/tmp/git-snapshot-ssh-status-a.json`, and `/tmp/git-snapshot-ssh-upstream-a.json`; retained
artifacts use the corresponding `-b.json` names.
## Publication hot-path retention audit
The deterministic publication benchmark compares sequential, already-settled
`GitStatusReadLeaseOwner` reads with `GitRepositorySnapshotOwner.readStatus` reads returning the
same representative result. It therefore isolates owner admission, immutable bounded copying,
freezing, and publication without measuring Git subprocess or parsing time. The retained harness
is `tools/benchmarks/git-repository-snapshot-owner-publication-bench.ts` and runs with:
```text
node --expose-gc --import tsx tools/benchmarks/git-repository-snapshot-owner-publication-bench.ts
```
On Node 24.18.0, Darwin arm64, Apple M4 Max, each arm received 2,000 / 400 / 50 warmup reads for
empty / 100-entry / 1,000-entry payloads. Timing then used 11 alternating rounds of 10,000 / 2,000
/ 250 reads per arm; allocation used seven rounds of 5,000 / 500 / 100 reads per arm with V8 heap
sampling at 128-byte intervals and an explicit GC before each profile. The table is the median of
the per-process medians from four independent process runs:
| Status entries | Lease CPU µs/read | Snapshot CPU µs/read | Incremental CPU µs/read | Incremental wall µs/read | Incremental sampled allocation/read |
| -------------: | ----------------: | -------------------: | ----------------------: | -----------------------: | ----------------------------------: |
| 0 | 0.455 | 1.198 | +0.743 | +0.683 | +1,612 bytes |
| 100 | 0.473 | 4.027 | +3.554 | +3.501 | +15,971 bytes |
| 1,000 | 0.510 | 29.464 | +28.954 | +28.485 | +151,086 bytes |
Incremental CPU ranges across the four processes were 0.710–0.804, 3.505–3.586, and
28.116–29.652 µs/read respectively. V8 sampling measures transient allocation rather than retained
heap; the exact immutable work is zero / 100 / 1,000 entry copies and six / 106 / 1,006
`Object.freeze` calls. The owner replaces the prior retained projection after each settled read,
so these temporary copies do not escape the existing one-projection-per-identity bound.
Every size returned the exact original `GitStatusResult` reference. JSON content and caller-owned
freeze state remained unchanged, while the snapshot, its status collection, and every retained
entry copy were separately frozen. This directly verifies that publication does not mutate or
substitute the current caller result.
No implementation optimization was retained. The 0.7 µs empty-result delta is too small to
justify conditional empty-array paths, and copying plus deep freezing are the mechanisms that keep
caller mutation out of retained snapshots. Deferring those operations until `getSnapshot` would
weaken the owner's uniformly immutable publication contract, while lazily loading query/freshness
code would make the synchronous exported live seam unavailable. The 10,250 raw / 2,043 gzip main
cost is fully main-owned; the former specialized upstream owner is removed, and preload plus
renderer remain byte-identical. At 29 µs for the 1,000-entry bound, publication is proportionate to
the process/RPC Git work and parsing it follows, without introducing a TTL cache or another
physical read.
## Production A/B
Fresh production builds were archived outside the worktree:
- A: `/tmp/orca-phase2-git-repository-snapshot-a.yuLlk5/out`
- B: `/tmp/orca-phase2-git-repository-snapshot-b-final.75XkBW/out`
| Artifact | A raw / gzip | B raw / gzip | Change | A SHA-256 | B SHA-256 |
| ---------------------- | ----------------- | ----------------- | ---------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
| Electron main entry | 779,267 / 174,769 | 789,517 / 176,812 | +10,250 / +2,043 | `64526204d377f8ca634de5940d77b3f2eff1421bd7a8e7346b0ffcc189be21da` | `6413904f6420882168b8993c1240614bacde1de92dfb26a55c4db67c84007b8e` |
| Electron preload entry | 130,798 / 20,642 | 130,798 / 20,642 | 0 / 0 | `c388a39cdca9609760e286d95b87ad1e53793720450e507f830ff1f6c5bd259f` | `c388a39cdca9609760e286d95b87ad1e53793720450e507f830ff1f6c5bd259f` |
The unchanged Electron-main raw budget is 825,109 bytes; B retains 35,592 bytes of headroom. File
counts remain 184 main, one preload, and 787 renderer artifacts.
| Tree | A SHA-256 | B SHA-256 |
| -------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
| Main | `7f6b86e1b736f43d8d259b4ff90f53dd78b0de7ab516dd0f54edf931034bfe2e` | `619e5977034ec79f679489666a826205500de88f51af39c3a3ccf99fbb68874c` |
| Preload | `06584e5cc8835554c7e0897efe5c578a9f66c3049033b2a3b79b1ca9a88462be` | `06584e5cc8835554c7e0897efe5c578a9f66c3049033b2a3b79b1ca9a88462be` |
| Renderer | `5f6156236d93a03256beb23df69ef705a78dd54a1290c58cc4330fbe8cc34e74` | `5f6156236d93a03256beb23df69ef705a78dd54a1290c58cc4330fbe8cc34e74` |
Preload and every renderer artifact are byte-identical. The renderer manifest is byte-identical
with SHA-256 `92ad617f8c8aee6d4c4a23622f524266cbe9c93b3fe65544d1acb833dc3f0d67`.
Complete A and B validation found 778 manifest records, three HTML entries, 6,247 static edges, 213
dynamic edges, and 860 emitted references, with zero missing or escaping targets, zero cross-entry
imports, and zero cycles.
Renderer entry measurements remain 8,416,540 raw / 1,877,823 gzip for index, 4,507,253 /
984,615 for popout, and 4,360,652 / 928,355 for web. JS/CSS counts remain 292/2, 77/2, and 33/1.
## Validation
- Current-behavior baseline suite: 211 passed.
- Final focused owner/status/upstream/provider suite: 228 passed.
- Physical-count candidate suite: 215 passed.
- Publication benchmark: four independent process runs; caller identity/immutability checks passed
for empty, 100-entry, and 1,000-entry results.
- Post-audit focused lease/snapshot-owner suite: 14 passed.
- Broad owner, status/cache, native/WSL, upstream, remote, SSH provider/dispatch/session,
filesystem mutation, and runtime-routing suite: 536 passed, 1 skipped.
- `pnpm run typecheck:node`: passed.
- Targeted `oxlint --deny-warnings`: passed.
- Targeted `oxfmt --check`: passed.
- `pnpm run check:max-lines-ratchet`: passed with 354 grandfathered suppressions and no new bypass.
- Fresh A and B `pnpm run build:electron-vite`: passed.
- Fresh post-audit production build reproduced B exactly: main 789,517 raw / 176,812 gzip with
SHA-256 `6413904f6420882168b8993c1240614bacde1de92dfb26a55c4db67c84007b8e`, preload
130,798 / 20,642 with SHA-256
`c388a39cdca9609760e286d95b87ad1e53793720450e507f830ff1f6c5bd259f`, and renderer
manifest SHA-256 `92ad617f8c8aee6d4c4a23622f524266cbe9c93b3fe65544d1acb833dc3f0d67`.
- `pnpm run check:electron-bundle-budgets`: passed against fresh B.
- Complete renderer manifest/path and entry-cycle validation: passed for A and B.
- `git diff --check`: passed.
## Next seam and limitations
The next slice can migrate Checks and active polling to the exported snapshot seam, then add
revision subscriptions and runtime/mobile projection transport. This tranche intentionally leaves
those consumers and all RPC surfaces unchanged.
- Revisions and generations are process-local and reset with the host owner.
- Local mutation invalidation remains intentionally coarse across the native/WSL owner; it may
mark unrelated retained projections stale, but it cannot merge identities or return cached data
through current APIs.
- Snapshot status retention may be a bounded prefix when the current caller explicitly disables
the Git status limit; current callers still receive the complete fresh result.
- Counts use mocked physical boundaries and do not measure live subprocess, WSL, or SSH latency.
- No packaged cross-platform or live SSH smoke was run.