checkout: surface silent rejections in batchAllSettled#2306
Conversation
📝 WalkthroughWalkthroughCheckout workdir/index update paths now treat per-task failures as collectable results: non-blocking writes use Promise.allSettled, all rejection reasons are logged (prefer Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/commands/checkout.js (1)
758-762: Minor: outer catch now double-logs the aggregated error.Each rejection is already logged with its stack inside the loop (lines 732-735), and the synthetic aggregated error thrown at line 752 will then be caught here and re-logged as
FATAL. That's not harmful, but the FATAL log adds little signal beyond the per-task logs above it. Consider only logging FATAL when the caught error is not the synthetic aggregate (i.e., an unexpected error escaped the inner try) — otherwise the per-rejection logs above are sufficient.This is a stylistic suggestion; current behavior is acceptable and matches the PR's stated intent.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/checkout.js` around lines 758 - 762, The outer catch (the catch (error) that logs `[isomorphic-git ${operationName}] FATAL:`) double-logs the synthetic aggregated error that was already logged per-rejection inside the loop; change the outer handler to only log when the caught error is not the synthetic aggregate (e.g., if !(error instanceof AggregateError) or the error lacks an .errors array), then rethrow in all cases. Locate the outer catch block and guard the console.error call accordingly so per-task rejection logs remain the only logs for aggregated failures while still logging unexpected non-aggregate exceptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/commands/checkout.js`:
- Around line 732-736: Prettier flags the ternary used in the console.error
call; replace the inline ternary with a Prettier-friendly expression such as
using optional chaining and nullish coalescing to pick the stack or the reason
(target the console.error call that references operationName and result.reason)
so the second argument becomes an expression like result.reason?.stack ??
result.reason to satisfy formatting rules.
---
Nitpick comments:
In `@src/commands/checkout.js`:
- Around line 758-762: The outer catch (the catch (error) that logs
`[isomorphic-git ${operationName}] FATAL:`) double-logs the synthetic aggregated
error that was already logged per-rejection inside the loop; change the outer
handler to only log when the caught error is not the synthetic aggregate (e.g.,
if !(error instanceof AggregateError) or the error lacks an .errors array), then
rethrow in all cases. Locate the outer catch block and guard the console.error
call accordingly so per-task rejection logs remain the only logs for aggregated
failures while still logging unexpected non-aggregate exceptions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8736323d-ec57-4524-8d18-882cced45a91
📒 Files selected for processing (1)
src/commands/checkout.js
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/commands/checkout.js (1)
278-354:⚠️ Potential issue | 🟠 MajorDefault (blocking) checkout path still silently swallows write errors — fix is incomplete.
batchAllSettledis only used in theif (nonBlocking)branch, butnonBlockingdefaults tofalse(line 56). In the defaultelsebranch the per-filetry { … } catch (e) { console.log(e) }at lines 348‑350 is unchanged, so the very failure mode this PR is meant to surface (per‑file write failures during checkout/clone reported as silent partial successes) is still present whenever callers don't opt intononBlocking: true.If the goal is to fix the linked downstream reports (clones appearing successful with empty/partial repos), the same rejection‑aggregation/throw treatment needs to be applied to the default branch as well — either by routing the default path through
batchAllSettledor by collecting and rethrowing errors from the inlinePromise.all(...)map. As written, most consumers (including_checkoutusers that don't passnonBlocking) will see no behavior change.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/checkout.js` around lines 278 - 354, The default (blocking) checkout path still swallows per-file write errors inside the Promise.all map in the GitIndexManager.acquire callback (the try/catch that just console.log(e)), so when nonBlocking is false callers won't see failures; update the else branch handling used by _checkout to either route the ops.map through the existing batchAllSettled helper (same behavior as the nonBlocking branch) or collect per-item rejections from the Promise.all and throw an aggregated Error after awaiting all ops, replacing the silent console.log in the map; look for symbols GitIndexManager.acquire, nonBlocking, batchAllSettled, and the ops.filter(...).map(...) block and ensure any caught errors are accumulated and rethrown (or use batchAllSettled) so the checkout fails on write errors.
🧹 Nitpick comments (1)
src/commands/checkout.js (1)
760-764: Outer try/catch produces duplicate logging without adding value.Every rejection is already
console.error'd individually inside the loop (lines 732‑737), and the aggregatedErrorthrown on line 754 already contains the operation name and a summary. The outercatchthen logs the same aggregated error again asFATALbefore rethrowing it unchanged — net effect is one extra log line per failed batch with no extra information.If the only intent is to ensure unexpected throws (e.g., from
onProgress) are visible, that's already achieved by callers' own error handling. Consider dropping the outer try/catch entirely, or restricting it to truly unexpected failures (i.e., don't relog the deliberately-thrown aggregate):♻️ Simplification
async function batchAllSettled(operationName, tasks, onProgress, batchSize) { const results = [] const rejections = [] - try { - for (let i = 0; i < tasks.length; i += batchSize) { - … - } - - if (rejections.length > 0) { - … - throw new Error(…) - } - - return results - } catch (error) { - // eslint-disable-next-line no-console - console.error(`[isomorphic-git ${operationName}] FATAL:`, error) - throw error - } + for (let i = 0; i < tasks.length; i += batchSize) { + … + } + if (rejections.length > 0) { + … + throw new Error(…) + } + return results }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/checkout.js` around lines 760 - 764, The outer try/catch that catches errors and does console.error(`[isomorphic-git ${operationName}] FATAL:`, error) should be removed (or at minimum the catch should not re-log already-aggregated failures) because each rejection is logged inside the loop and an aggregated Error is explicitly thrown later; locate the try/catch surrounding the loop that references operationName and remove the catch block (or change it to simply rethrow without logging), leaving the loop-level logging and the aggregated Error throw intact so unexpected errors still propagate from functions like onProgress but are not double-logged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/commands/checkout.js`:
- Around line 749-757: Replace the plain Error thrown for multiple rejections
with the project’s MultipleGitError so callers can access structured failures;
import MultipleGitError if missing, keep the existing human-readable message
(using operationName, tasks.length and the summary) but construct and throw a
MultipleGitError that carries the original rejections in its .errors and .data
fields (follow the pattern used in src/api/add.js) so programmatic consumers and
GitError wrapper can inspect the underlying errors.
---
Outside diff comments:
In `@src/commands/checkout.js`:
- Around line 278-354: The default (blocking) checkout path still swallows
per-file write errors inside the Promise.all map in the GitIndexManager.acquire
callback (the try/catch that just console.log(e)), so when nonBlocking is false
callers won't see failures; update the else branch handling used by _checkout to
either route the ops.map through the existing batchAllSettled helper (same
behavior as the nonBlocking branch) or collect per-item rejections from the
Promise.all and throw an aggregated Error after awaiting all ops, replacing the
silent console.log in the map; look for symbols GitIndexManager.acquire,
nonBlocking, batchAllSettled, and the ops.filter(...).map(...) block and ensure
any caught errors are accumulated and rethrown (or use batchAllSettled) so the
checkout fails on write errors.
---
Nitpick comments:
In `@src/commands/checkout.js`:
- Around line 760-764: The outer try/catch that catches errors and does
console.error(`[isomorphic-git ${operationName}] FATAL:`, error) should be
removed (or at minimum the catch should not re-log already-aggregated failures)
because each rejection is logged inside the loop and an aggregated Error is
explicitly thrown later; locate the try/catch surrounding the loop that
references operationName and remove the catch block (or change it to simply
rethrow without logging), leaving the loop-level logging and the aggregated
Error throw intact so unexpected errors still propagate from functions like
onProgress but are not double-logged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a025b12b-b560-4e5a-9988-70b42656853b
📒 Files selected for processing (1)
src/commands/checkout.js
|
Thanks for the fix. The code looks good. Can you apply changes suggested by CodeRabbit? If you don't agree with the suggestions, please give a reason. |
- Apply rejection collection to default (blocking) branch too — previously
only the nonBlocking path used batchAllSettled, so the original silent
catch (e) { console.log(e) } in the else branch still swallowed per-file
write errors for default callers (the very failure mode this PR targets).
- Throw MultipleGitError instead of a plain Error in batchAllSettled, so
callers can introspect .errors / .data programmatically (matches the
pattern used in src/api/add.js).
- Drop the outer try/catch around the loop in batchAllSettled to avoid
double-logging the synthetic aggregate (each rejection is already logged
per-task inside the loop).
- Replace inline ternary with optional chaining + nullish coalescing in
the per-task console.error.
|
@jcubic thanks for the review! Pushed 40d73a3 addressing CodeRabbit's feedback:
ESLint and Prettier both pass locally on |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/commands/checkout.js (2)
758-758: Optional: enrich the aggregated error message with operation context.
new MultipleGitError(rejections)produces a fixed generic message (There are multiple errors that were thrown by the method...), so unhandled-rejection logs and stack traces lose theoperationNameand then/total failedsummary that the original PR description called out. Programmatic consumers still get full detail via.errors/.data, so this is purely a diagnostics nicety — feel free to skip if you prefer to keepMultipleGitErrorinstances uniform.💡 Optional message augmentation
if (rejections.length > 0) { - throw new MultipleGitError(rejections) + const err = new MultipleGitError(rejections) + err.message = `[isomorphic-git ${operationName}] ${rejections.length}/${tasks.length} tasks failed: ${err.message}` + throw err }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/checkout.js` at line 758, The throw currently does "throw new MultipleGitError(rejections)" which yields a generic message; instead build a concise diagnostic string that includes the operationName and a "failed n/total" summary (e.g., `${operationName}: ${rejections.length}/${total} failed`) and attach it to the thrown error: either pass it into MultipleGitError if its constructor accepts a message, or create the MultipleGitError instance and set its message property (and/or a .summary field) before throwing; use the existing symbols MultipleGitError and rejections (and operationName / total in the surrounding scope) so logs and unhandled-rejection traces show the enriched context.
350-363: Extract the duplicated rejection-handling logic, and unify the log prefix.The rejection-collection / per-task logging / aggregated throw is now duplicated between the blocking branch (lines 350‑363) and
batchAllSettled(lines 736‑747, 757‑759). Extracting a small helper keeps the two paths in lockstep when this evolves (e.g., adding fail-fast, telemetry, or message customization).Minor consistency nit: the blocking branch logs
[isomorphic-git checkout] task rejected:(hard-coded) whilebatchAllSettledlogs[isomorphic-git ${operationName}](e.g., "Update Working Dir"). Passing anoperationNamethrough to the helper makes log filtering uniform across both paths.♻️ Sketch: shared helper
+function collectRejections(operationName, settled) { + const results = [] + const rejections = [] + for (const result of settled) { + if (result.status === 'fulfilled') { + results.push(result.value) + } else { + rejections.push(result.reason) + // eslint-disable-next-line no-console + console.error( + `[isomorphic-git ${operationName}] task rejected:`, + result.reason?.stack ?? result.reason + ) + } + } + return { results, rejections } +}Then both call sites become:
- const rejections = [] - for (const result of settled) { - if (result.status === 'rejected') { - rejections.push(result.reason) - // eslint-disable-next-line no-console - console.error( - '[isomorphic-git checkout] task rejected:', - result.reason?.stack ?? result.reason - ) - } - } - if (rejections.length > 0) { - throw new MultipleGitError(rejections) - } + const { rejections } = collectRejections('checkout', settled) + if (rejections.length > 0) throw new MultipleGitError(rejections)Also applies to: 730-761
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/checkout.js` around lines 350 - 363, Extract the duplicated rejection collection, per-task logging and aggregated throwing into a small helper (e.g., handleSettledRejections(settled, operationName)) and replace the duplicated blocks in the blocking branch inside checkout.js (the loop that builds rejections and calls console.error then throws MultipleGitError) and the batchAllSettled code paths with calls to that helper; the helper should iterate settled results, push rejected reasons into an array, log each rejection using a unified prefix that uses operationName (format like `[isomorphic-git ${operationName}] task rejected:`) and, if any rejections exist, throw new MultipleGitError(rejections). Ensure both callers pass a descriptive operationName (e.g., "checkout" or the existing operationName used by batchAllSettled) so log filtering is consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/commands/checkout.js`:
- Line 758: The throw currently does "throw new MultipleGitError(rejections)"
which yields a generic message; instead build a concise diagnostic string that
includes the operationName and a "failed n/total" summary (e.g.,
`${operationName}: ${rejections.length}/${total} failed`) and attach it to the
thrown error: either pass it into MultipleGitError if its constructor accepts a
message, or create the MultipleGitError instance and set its message property
(and/or a .summary field) before throwing; use the existing symbols
MultipleGitError and rejections (and operationName / total in the surrounding
scope) so logs and unhandled-rejection traces show the enriched context.
- Around line 350-363: Extract the duplicated rejection collection, per-task
logging and aggregated throwing into a small helper (e.g.,
handleSettledRejections(settled, operationName)) and replace the duplicated
blocks in the blocking branch inside checkout.js (the loop that builds
rejections and calls console.error then throws MultipleGitError) and the
batchAllSettled code paths with calls to that helper; the helper should iterate
settled results, push rejected reasons into an array, log each rejection using a
unified prefix that uses operationName (format like `[isomorphic-git
${operationName}] task rejected:`) and, if any rejections exist, throw new
MultipleGitError(rejections). Ensure both callers pass a descriptive
operationName (e.g., "checkout" or the existing operationName used by
batchAllSettled) so log filtering is consistent.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d8a7dcf8-1cdc-41ad-9326-e950946af69b
📒 Files selected for processing (1)
src/commands/checkout.js
|
🎉 This PR is included in version 1.37.6 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Problem
batchAllSettledinsrc/commands/checkout.jssilently discards allrejectedpromise results fromPromise.allSettled, returning only the fulfilled values. Combined with the function's outercatchthat onlyconsole.errors and falls through toreturn results, every individual file write that fails during checkout is swallowed without surfacing — to either the caller or the user.This contributes to a long-standing class of "clone appears to succeed but vault is empty / partially populated" bug reports across downstream consumers, especially on resource-constrained runtimes like iOS WKWebView where individual
fs.writecalls can fail under memory pressure mid-checkout.Notable downstream reports referencing this exact symptom:
Change
batchAllSettlednow:rejectionsarray.console.errors each individual rejection with its stack (preserving previous diagnostic noise level for users running with verbose logging).Errorif any task rejected, including the count and first 5 error messages.resultsarray.This converts a class of silent failures into actionable errors. Behaviour for the all-success path is unchanged.
Risk
Callers that were silently relying on partial-success results (i.e. swallowing a checkout that left files un-written) will now receive an exception instead of a misleading success. This is the intended fix — there should be no caller relying on the previous behaviour, since they had no way to know it was happening.
Summary by CodeRabbit