From b6a00b350faf2148d41dca4eaf164a39feb497ff Mon Sep 17 00:00:00 2001 From: Jason Barnett Date: Fri, 20 Mar 2026 16:52:56 +0000 Subject: [PATCH] Add retry logic to checkout and submoduleUpdate for partial clone resilience When using partial clones (filter=blob:none, which is automatically set for sparse checkouts), `git checkout` lazily fetches missing blobs from the promisor remote. If the remote is temporarily unavailable, this network call fails and surfaces as a hard error with no retry. The `fetch`, `getDefaultBranch`, and `lfsFetch` methods already use retryHelper, but `checkout` and `submoduleUpdate` did not, despite both performing network operations: - `checkout`: fetches blobs on-demand from promisor remotes during partial clone checkouts - `submoduleUpdate`: clones/fetches submodule repositories This was observed in production when GitHub's git service had a brief outage, causing the checkout step to fail with: fatal: unable to access '...': Failed to connect to github.com port 443 after 135272 ms: Couldn't connect to server fatal: could not fetch from promisor remote Wrapping both methods with the existing retryHelper (3 attempts with 10-20s jittered backoff) makes these operations resilient to transient network failures, consistent with how fetch already behaves. Co-Authored-By: Claude Opus 4.6 (1M context) --- dist/index.js | 14 ++++++++++++-- src/git-command-manager.ts | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/dist/index.js b/dist/index.js index fe3f317..6f39780 100644 --- a/dist/index.js +++ b/dist/index.js @@ -789,7 +789,14 @@ class GitCommandManager { else { args.push(ref); } - yield this.execGit(args); + // Retry checkout because it can trigger network I/O when using partial + // clones (filter=blob:none). In that mode git lazily fetches missing + // blobs from the promisor remote during checkout, so a transient network + // failure would otherwise surface as a hard error here. + const that = this; + yield retryHelper.execute(() => __awaiter(this, void 0, void 0, function* () { + yield that.execGit(args); + })); }); } checkoutDetach() { @@ -990,7 +997,10 @@ class GitCommandManager { if (recursive) { args.push('--recursive'); } - yield this.execGit(args); + const that = this; + yield retryHelper.execute(() => __awaiter(this, void 0, void 0, function* () { + yield that.execGit(args); + })); }); } submoduleStatus() { diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index f5ba40e..6f9578c 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -228,7 +228,14 @@ class GitCommandManager { args.push(ref) } - await this.execGit(args) + // Retry checkout because it can trigger network I/O when using partial + // clones (filter=blob:none). In that mode git lazily fetches missing + // blobs from the promisor remote during checkout, so a transient network + // failure would otherwise surface as a hard error here. + const that = this + await retryHelper.execute(async () => { + await that.execGit(args) + }) } async checkoutDetach(): Promise { @@ -457,7 +464,10 @@ class GitCommandManager { args.push('--recursive') } - await this.execGit(args) + const that = this + await retryHelper.execute(async () => { + await that.execGit(args) + }) } async submoduleStatus(): Promise {