function runGit
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
}

Parameters

args: string[]
cwd: string
optional
timeoutMs = GIT_TIMEOUT_MS
optional
command: string = git

Return Type

Task<string>

Usage

import { runGit } from "lib/utils/get-changed-file-paths-in-git-since.ts";