mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
Three main-process call sites list a repo's worktrees to read `worktree.path` and nothing else, but went through the annotated listing, so each one paid a sparse-checkout probe per worktree and cached the result nobody consumed: - `registered-worktree-roots-cache.ts` rebuilds the filesystem-auth authorized roots. `invalidateAuthorizedRootsCache()` fires on every worktree create and remove, plus repo add/clone/settings changes, so this reruns constantly. - `filesystem-source-control-ai-targets.ts` checks whether a local repo owns a worktree path. - `hosted-review.ts` verifies a worktree belongs to the repo before granting access. The probe is an `fs.stat` of the per-worktree `info/sparse-checkout` plus, when that file is non-empty, a git config read. On a WSL-hosted repo both cross 9p. #17859 cached it and #17932 keyed that cache on the distro, which fixed a wrong answer but also meant the distro-less callers above populate a second entry per worktree — probed cold, revalidated on their own five-minute loop, and read by nobody. Worktree create/remove clears the sparse cache and dirties the roots cache together, so both variants go cold at once and the discarded half is re-probed in full on the next auth check. `listRepoWorktreeGraph` routes those callers to `listWorktreeGraph`, which already existed as the annotation-free listing (#17655). Doing only that would have cost a second `git worktree list`. The scan cache keys in-flight scans on a `kind`, and graph and lenient were separate kinds, so a roots rebuild overlapping a sidebar refresh would spawn its own subprocess where the two previously coalesced. That is a real regression on macOS, Linux and native Windows, where `getLocalProjectWorktreeGitOptions` returns `{}` and both callers land on the identical key; on WSL they already differ by distro and never shared. So the annotated listing is now the graph listing plus annotation, rather than a parallel scan of its own: `listWorktrees` awaits `listWorktreeGraph` and annotates the rows it returns. Both soften a Git failure to `[]`, so they can share one listing; strict keeps its own because it must be able to reject. The two kinds ran Git twice before and now run it once, so the overlap case gets strictly faster instead of paying for the opt-out. An annotated scan holds two in-flight entries now (its own, plus the graph listing it shares). Keeping its own entry matters: `detectSparseCheckoutCached` dedupes revalidation but not the initial fill, so two concurrent badge readers sharing only the graph scan would both probe. Per-platform delta: - macOS/Linux: fewer probes on the three call sites; one `git worktree list` instead of two when a graph and an annotated scan overlap. - native Windows, no WSL: same, and the saved subprocess is the expensive half. - Windows + WSL: the largest win. The discarded probes were 9p round-trips re-paid cold after every worktree create/remove. - SSH/relay: none. `listRepoWorktreeGraph` returns through the same provider branch as `listRepoWorktrees` before reaching local Git. - folder workspaces: none. Both return the same synthetic folder worktree. Not in this change: - The badge listing itself. It still probes, still annotates, and still keys on the distro exactly as #17932 left it. - The remaining `listRepoWorktrees` callers. They read `isSparse`, or feed rows to something that does.
94 lines
3.4 KiB
TypeScript
94 lines
3.4 KiB
TypeScript
// The annotated listing is the graph listing plus a sparse probe: callers that read only
|
|
// `worktree.path` must skip the probe, without costing a second `git worktree list`.
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import type { GitWorktreeInfo } from '../../shared/worktree/types'
|
|
|
|
const { detectSparseCheckoutMock, readWorktreeListMock, readTranslatedWorktreeGraphMock } =
|
|
vi.hoisted(() => ({
|
|
detectSparseCheckoutMock: vi.fn(),
|
|
readWorktreeListMock: vi.fn(),
|
|
readTranslatedWorktreeGraphMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('./worktree-sparse-state', () => ({
|
|
detectSparseCheckout: detectSparseCheckoutMock,
|
|
resolveGitCommonDir: vi.fn()
|
|
}))
|
|
vi.mock('./worktree-list-reader', () => ({
|
|
readCheckedOutBranchRef: vi.fn(),
|
|
readRepoCommonDirFromGit: vi.fn(),
|
|
readRepoLocation: vi.fn(),
|
|
readTranslatedWorktreeGraph: readTranslatedWorktreeGraphMock,
|
|
readWorktreeHeadOid: vi.fn(),
|
|
readWorktreeList: readWorktreeListMock
|
|
}))
|
|
|
|
import { _resetWorktreeScanCacheForTests, listWorktreeGraph, listWorktrees } from './worktree'
|
|
import { __resetSparseCheckoutStateCacheForTests } from './worktree-sparse-checkout-cache'
|
|
|
|
const REPO = '\\\\wsl.localhost\\Ubuntu\\home\\me\\repo'
|
|
const ROW: GitWorktreeInfo = {
|
|
path: 'C:\\wt\\x',
|
|
head: 'a'.repeat(40),
|
|
branch: 'refs/heads/feature',
|
|
isBare: false,
|
|
isMainWorktree: false
|
|
}
|
|
|
|
describe('graph and annotated worktree scans', () => {
|
|
beforeEach(() => {
|
|
detectSparseCheckoutMock.mockReset()
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
readWorktreeListMock.mockReset()
|
|
readWorktreeListMock.mockResolvedValue([ROW])
|
|
readTranslatedWorktreeGraphMock.mockReset()
|
|
readTranslatedWorktreeGraphMock.mockResolvedValue([ROW])
|
|
_resetWorktreeScanCacheForTests()
|
|
__resetSparseCheckoutStateCacheForTests()
|
|
})
|
|
|
|
it('does not probe sparse state for a graph scan', async () => {
|
|
const rows = await listWorktreeGraph(REPO, { wslDistro: 'Ubuntu' })
|
|
|
|
expect(rows[0]?.path).toBe('C:\\wt\\x')
|
|
expect(rows[0]?.isSparse).toBeUndefined()
|
|
expect(detectSparseCheckoutMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('still probes sparse state for the annotated scan', async () => {
|
|
const rows = await listWorktrees(REPO, { wslDistro: 'Ubuntu' })
|
|
|
|
expect(rows[0]?.isSparse).toBe(true)
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('reads the git listing once for an overlapping graph and annotated scan', async () => {
|
|
const [graphRows, annotatedRows] = await Promise.all([
|
|
listWorktreeGraph(REPO, { wslDistro: 'Ubuntu' }),
|
|
listWorktrees(REPO, { wslDistro: 'Ubuntu' })
|
|
])
|
|
|
|
expect(readTranslatedWorktreeGraphMock).toHaveBeenCalledTimes(1)
|
|
expect(graphRows[0]?.isSparse).toBeUndefined()
|
|
expect(annotatedRows[0]?.isSparse).toBe(true)
|
|
})
|
|
|
|
// Sharing the listing must not make the probe-free caller wait on the probe it opted out of.
|
|
it('resolves a graph scan while the annotated scan is still probing', async () => {
|
|
let releaseProbe!: () => void
|
|
detectSparseCheckoutMock.mockImplementation(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
releaseProbe = () => resolve(true)
|
|
})
|
|
)
|
|
|
|
const annotatedScan = listWorktrees(REPO, { wslDistro: 'Ubuntu' })
|
|
const graphRows = await listWorktreeGraph(REPO, { wslDistro: 'Ubuntu' })
|
|
|
|
expect(graphRows[0]?.path).toBe('C:\\wt\\x')
|
|
releaseProbe()
|
|
expect((await annotatedScan)[0]?.isSparse).toBe(true)
|
|
})
|
|
})
|