runGit(args: string[],cwd: string,timeoutMs?,command?: string): Task<string>
Runs one git command with a hard upper bound on how long it can take.
Two layers, because they cover different failures: execFile's own timeout kills a child
that is merely slow, while the outer race guarantees this run settles even if the child's
exit event never arrives — the case where killing the child doesn't help because nobody is
listening for it to die. That second layer is what turns an unkillable hang into a normal
rejection the caller already knows how to degrade on.
Returns a Task, so no subprocess is spawned until something awaits it, and a single
invocation can be retried on its own (index.lock contention is per-call, not per-scan).
It stays the low-level primitive either way: it rejects with a raw Error, never a declared
Failure — classifying that is the job of the producers built on it. And because a Task is
a Promise, await runGit(...) reads exactly as it did.
// Defined, not invoked: awaiting the Task spawns the git subprocess. async function headDiff(projectRoot: string): Promise<string> { return await runGit(['diff', '--name-only', 'HEAD'], projectRoot); // git's stdout }
Task<string>