repositories / will
will
owned by admin
src/agent/worktree.ts
Rawimport { execFile } from 'node:child_process'
import { existsSync } from 'node:fs'
import { join } from 'node:path'
import { promisify } from 'node:util'
import { createLogger } from '../shared/log.ts'
const exec = promisify(execFile)
const log = createLogger({ component: 'worktree' })
export interface WorktreeStatus {
head?: string | undefined
dirty: boolean
diverged: boolean
fastForwarded: boolean
cloned: boolean
error?: string | undefined
}
/**
* Cheap local read of the current worktree state (rev-parse + status; no
* fetch, no merge). Used to refresh wake facts between boot syncs so they
* never report a stale HEAD. Never throws; failures land in `error`.
*/
export async function readWorktreeState(workDir: string): Promise<WorktreeStatus> {
const status: WorktreeStatus = {
dirty: false,
diverged: false,
fastForwarded: false,
cloned: false,
}
try {
const git = (...args: string[]) => exec('git', args, { cwd: workDir, maxBuffer: 10 << 20 })
status.head = (await git('rev-parse', 'HEAD')).stdout.trim()
const dirty = await git('status', '--porcelain')
status.dirty = dirty.stdout.trim().length > 0
} catch (err) {
status.error = err instanceof Error ? err.message : String(err)
}
return status
}
/**
* Startup worktree sync (spec AC15): fetch; fast-forward only when clean;
* dirty or divergent work is preserved and reported, never reset.
*/
export async function syncWorktree(
workDir: string,
gitUrl: string | undefined,
branch: string,
): Promise<WorktreeStatus> {
const status: WorktreeStatus = {
dirty: false,
diverged: false,
fastForwarded: false,
cloned: false,
}
const git = (cwd: string | undefined, ...args: string[]) =>
exec('git', args, { cwd, maxBuffer: 10 << 20 })
try {
if (!existsSync(join(workDir, '.git'))) {
if (!gitUrl) {
status.error = 'worktree missing and no git url configured'
return status
}
await exec('git', ['clone', '--branch', branch, gitUrl, workDir], { maxBuffer: 10 << 20 })
status.cloned = true
}
if (gitUrl) await git(workDir, 'fetch', 'origin', branch).catch(() => undefined)
const head = await git(workDir, 'rev-parse', 'HEAD')
status.head = head.stdout.trim()
const dirty = await git(workDir, 'status', '--porcelain')
status.dirty = dirty.stdout.trim().length > 0
if (gitUrl && !status.dirty) {
const behind = await git(workDir, 'rev-list', '--count', `HEAD..origin/${branch}`)
const ahead = await git(workDir, 'rev-list', '--count', `origin/${branch}..HEAD`)
if (Number(behind.stdout.trim()) > 0 && Number(ahead.stdout.trim()) === 0) {
await git(workDir, 'merge', '--ff-only', `origin/${branch}`)
status.fastForwarded = true
status.head = (await git(workDir, 'rev-parse', 'HEAD')).stdout.trim()
} else if (Number(ahead.stdout.trim()) > 0) {
status.diverged = true
}
}
return status
} catch (err) {
status.error = err instanceof Error ? err.message : String(err)
log.warn('worktree sync failed', { error: status.error })
return status
}
}