Files
orca/src/main/ipc/worktree-git-common-primary-polling.ts
T
Brennan Benson de64337c26 fix(worktree-watcher): refresh status after external pushes (#12361)
* fix(worktree-watcher): surface external push -u through the git-common watch

An external-shell 'git push -u' writes only the common .git/config (plus
refs/remotes/<remote>/<branch>), both invisible to the git-common event
filter, so the Checks panel stayed on 'No upstream configured' until the
renderer safety poll. Classify the common config and remote-tracking refs
as status-tier signals, poll config alongside the other primary-checkout
metadata files, and keep FETCH_HEAD/reflog/ref-lock churn ignored.

* fix(worktree-watcher): refresh after subsequent pushes
2026-08-05 11:26:13 -07:00

149 lines
4.1 KiB
TypeScript

import { stat } from 'node:fs/promises'
import { join } from 'node:path'
import type {
WorktreeBasePollEvent,
WorktreeBaseSubscription,
WorktreePollerWindowVisibility
} from './worktree-base-directory-poller'
import { PRIMARY_CHECKOUT_METADATA_FILES } from './worktree-git-common-polling'
type PrimaryMetadataSnapshot = {
signatures: Map<string, string>
statusRefPaths: Set<string>
}
async function snapshotPrimaryCheckoutMetadata(
commonDirPath: string,
getStatusRefPaths: () => readonly string[]
): Promise<PrimaryMetadataSnapshot> {
const signatures = new Map<string, string>()
const statusRefPaths = new Set(getStatusRefPaths())
const paths = [
...PRIMARY_CHECKOUT_METADATA_FILES.map((name) => join(commonDirPath, name)),
...statusRefPaths
]
await Promise.all(
paths.map(async (filePath) => {
try {
const value = await stat(filePath)
if (value.isFile()) {
signatures.set(filePath, `${value.mtimeMs}:${value.ctimeMs}:${value.ino}:${value.size}`)
}
} catch {
// Missing dynamic leaves remain selected so later creation is visible.
}
})
)
return { signatures, statusRefPaths }
}
function classifyMetadataSignatureDiff(
prevSignature: string | undefined,
nextSignature: string | undefined
): WorktreeBasePollEvent['type'] | null {
if (prevSignature === undefined && nextSignature === undefined) {
return null
}
if (prevSignature === undefined) {
return 'create'
}
if (nextSignature === undefined) {
return 'delete'
}
return prevSignature === nextSignature ? null : 'update'
}
function diffPrimaryMetadata(
prev: PrimaryMetadataSnapshot,
next: PrimaryMetadataSnapshot
): WorktreeBasePollEvent[] {
const events: WorktreeBasePollEvent[] = []
const paths = new Set([...prev.signatures.keys(), ...next.signatures.keys()])
for (const path of paths) {
const isStatusRef = prev.statusRefPaths.has(path) || next.statusRefPaths.has(path)
if (isStatusRef && (!prev.statusRefPaths.has(path) || !next.statusRefPaths.has(path))) {
continue
}
const type = classifyMetadataSignatureDiff(prev.signatures.get(path), next.signatures.get(path))
if (type) {
events.push({ type, path })
}
}
return events
}
export async function startGitCommonPrimaryPolling(
commonDirPath: string,
getStatusRefPaths: () => readonly string[],
onEvents: (events: WorktreeBasePollEvent[]) => void,
pollIntervalMs: number,
visibility: WorktreePollerWindowVisibility,
onFullScan?: () => void
): Promise<WorktreeBaseSubscription> {
let disposed = false
let ticking = false
let snapshot = await snapshotPrimaryCheckoutMetadata(commonDirPath, getStatusRefPaths)
let timer: ReturnType<typeof setTimeout> | null = null
let parkedWhileHidden = false
const tick = async (): Promise<void> => {
timer = null
if (disposed) {
return
}
if (!visibility.isWindowVisible()) {
parkedWhileHidden = true
return
}
if (ticking) {
return
}
ticking = true
const startedAt = Date.now()
onFullScan?.()
try {
const next = await snapshotPrimaryCheckoutMetadata(commonDirPath, getStatusRefPaths)
if (disposed) {
return
}
const events = diffPrimaryMetadata(snapshot, next)
snapshot = next
if (events.length > 0) {
onEvents(events)
}
} catch {
// Transient fs error: keep the previous snapshot and retry next tick.
} finally {
ticking = false
}
if (!disposed) {
const nextDelay = Math.max(
0,
Math.min(pollIntervalMs, pollIntervalMs - (Date.now() - startedAt))
)
timer = setTimeout(() => void tick(), nextDelay)
timer.unref?.()
}
}
const unsubscribeVisibility = visibility.onWindowBecameVisible(() => {
if (disposed || !parkedWhileHidden) {
return
}
parkedWhileHidden = false
void tick()
})
timer = setTimeout(() => void tick(), pollIntervalMs)
timer.unref?.()
return {
unsubscribe: async () => {
disposed = true
if (timer) {
clearTimeout(timer)
}
unsubscribeVisibility()
}
}
}