action: extend shallow-unreachable deepen-retry to checkout_pr fetches (#734)

extracts the deepen-retry helper from `GitFetchTool` into shared
`$gitFetchWithDeepen` and applies it to every fetch in `checkoutPrBranch`
(baseRef, pull/N/head, before_sha temp branch). on shallow clones with
deep PR ancestry — the failure mode behind ~10 of 51 `heuristic:very-slow`
runs in 24h on `remotion-dev/remotion` — the baseRef fetch was throwing
`Could not read <sha>` to the agent before the compare-api deepen block
could run. agents then burned 10+ minutes retrying `checkout_pr` and
falling back to ad-hoc shell `git fetch --deepen` workarounds.

also splits the analyzer's `heuristic:git-error-recovered` into
`heuristic:git-shallow-unreachable` and `heuristic:git-shallow-lock`
buckets so future audits surface this without manual log-grep.

closes #656.
This commit is contained in:
Colin McDonnell
2026-05-14 03:44:08 +00:00
committed by pullfrog[bot]
parent 2960d51493
commit 4ad649ebb9
3 changed files with 81 additions and 42 deletions
+54
View File
@@ -14,6 +14,7 @@ import { readFileSync, realpathSync, unlinkSync } from "node:fs";
import { log } from "./cli.ts";
import type { GitAuthServer } from "./gitAuthServer.ts";
import { filterEnv } from "./secrets.ts";
import { $ } from "./shell.ts";
import { spawn } from "./subprocess.ts";
type SafeGitSubcommand = "fetch" | "push";
@@ -181,3 +182,56 @@ export async function $git(
}
}
}
/**
* shallow-clone unreachable: when an existing local depth is too shallow for
* git to traverse to the requested ref's ancestry, the remote walk fails with
* one of these wordings (git emits the full OID via oid_to_hex, so the bound
* is 40 for SHA-1 or 64 for SHA-256). detecting both lets a single deepen
* retry recover before the error reaches the agent — see issue #564 for the
* original `git_fetch` precedent and #656 for the `checkout_pr` follow-up.
*/
export const SHALLOW_UNREACHABLE_PATTERNS: RegExp[] = [
/Could not read [a-f0-9]{40,64}/,
/remote did not send all necessary objects/,
];
/**
* large enough to clear the merge base on most real-world PRs without
* downloading the full history; matches the fallback used by
* `checkoutPrBranch` when the GitHub compare API is unavailable.
*/
export const DEEPEN_RETRY_DEPTH = 1000;
/**
* authenticated `git fetch` that recovers from shallow-unreachable errors
* by retrying once with `--deepen=1000`. callers pass the same args they
* would to `$git("fetch", ...)`; on shallow-unreachable failures in a
* shallow repo, the second attempt prepends `--deepen=N` and strips any
* caller-supplied `--depth=` (the two flags are mutually exclusive, and
* the caller's depth is what got us into this mess).
*
* non-shallow-unreachable errors and non-shallow repos rethrow unchanged,
* so this is safe to wrap any fetch without changing fast-path behavior.
*/
export async function $gitFetchWithDeepen(
args: string[],
options: GitAuthOptions,
label?: string
): Promise<GitResult> {
try {
return await $git("fetch", args, options);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
const isShallowUnreachable = SHALLOW_UNREACHABLE_PATTERNS.some((p) => p.test(msg));
if (!isShallowUnreachable) throw err;
const isShallow =
$("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
if (!isShallow) throw err;
log.info(
`» ${label ?? "git fetch"} hit shallow-unreachable error, retrying with --deepen=${DEEPEN_RETRY_DEPTH}`
);
const retryArgs = args.filter((a) => !a.startsWith("--depth="));
return await $git("fetch", [`--deepen=${DEEPEN_RETRY_DEPTH}`, ...retryArgs], options);
}
}