Luigit
repositories / will

will

owned by admin

src/agent/worktree.ts

Raw
import { 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
  }
}