1// Thin wrappers over the git CLI. Every call is scoped to one bare repo.2import { spawnSync } from 'node:child_process';3import type { Blob } from './types.ts';45const MAX = 1 << 30;67interface RunOptions {8 input?: string;9 allowFail?: boolean;10}1112export function run(gitDir: string, args: string[], { input, allowFail = false }: RunOptions = {}): string {13 const r = spawnSync('git', ['-c', 'core.quotepath=false', '--git-dir', gitDir, ...args], { input, maxBuffer: MAX, encoding: 'utf8' });14 if (r.status !== 0) {15 if (allowFail) return '';16 throw new Error(`git ${args.join(' ')} (${gitDir}): ${r.stderr?.toString().trim()}`);17 }18 return r.stdout;19}2021// Like run, but null on failure.22export function tryRun(gitDir: string, args: string[], opts: RunOptions = {}): string | null {23 const r = spawnSync('git', ['-c', 'core.quotepath=false', '--git-dir', gitDir, ...args], { input: opts.input, maxBuffer: MAX, encoding: 'utf8' });24 return r.status === 0 ? r.stdout : null;25}2627function runBuffer(gitDir: string, args: string[], input: string): Buffer {28 const r = spawnSync('git', ['-c', 'core.quotepath=false', '--git-dir', gitDir, ...args], { input, maxBuffer: MAX });29 if (r.status !== 0) throw new Error(`git ${args.join(' ')} (${gitDir}): ${r.stderr?.toString().trim()}`);30 return r.stdout;31}3233export const ZERO = '0'.repeat(40);3435// Read many objects in one process.36export function catFileBatch(gitDir: string, oids: string[]): Map<string, Blob> {37 const out = new Map<string, Blob>();38 const uniq = [...new Set(oids)];39 if (!uniq.length) return out;40 const buf = runBuffer(gitDir, ['cat-file', '--batch'], uniq.join('\n') + '\n');41 let i = 0;42 while (i < buf.length) {43 const nl = buf.indexOf(10, i);44 const [oid, type, size] = buf.subarray(i, nl).toString().split(' ');45 if (type === 'missing') {46 i = nl + 1;47 continue;48 }49 const n = Number(size);50 out.set(oid, { type, data: buf.subarray(nl + 1, nl + 1 + n) });51 i = nl + 1 + n + 1;52 }53 return out;54}5556export function isAncestor(gitDir: string, a: string, b: string): boolean {57 return spawnSync('git', ['--git-dir', gitDir, 'merge-base', '--is-ancestor', a, b]).status === 0;58}5960export function revList(gitDir: string, args: string[]): string[] {61 const s = tryRun(gitDir, ['rev-list', ...args]);62 return s ? s.split('\n').filter(Boolean) : [];63}6465// Binary heuristic matching git's: a NUL byte in the first 8000 bytes.66export function isBinary(buf: Buffer): boolean {67 return buf.subarray(0, 8000).includes(0);68}