Skip to content

fix(engine): capture baseCommitSha against local main, not origin/main#1376

Merged
gsxdsm merged 3 commits into
mainfrom
gsxdsm/fileschanged
Jun 4, 2026
Merged

fix(engine): capture baseCommitSha against local main, not origin/main#1376
gsxdsm merged 3 commits into
mainfrom
gsxdsm/fileschanged

Conversation

@gsxdsm

@gsxdsm gsxdsm commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

In-review tasks on the board could show 20–31 "files changed" when they actually touched 2–12 — the extra files belonged to other tasks that had already merged. Task branches fork from local main, but baseCommitSha was captured as merge-base(HEAD, origin/main): whenever local main carried merged-but-unpushed task commits, the recorded base rewound past them, and once the post-merge rebase-and-push rewrote those SHAs, baseCommitSha..HEAD permanently swept the predecessors' files into the new task's diff (display recovery can't tighten because the orphaned SHAs are no longer in main).

The capture now measures against local main first with origin/main as fallback — matching the two contamination-base sites (worktree-acquisition.ts, auto-recovery-handlers/branch-worktree.ts) that were already local-first; this was the only origin-first site. The logic is extracted into base-commit-capture.ts with a real-git regression suite, including the local-ahead-of-origin scenario that fails against the old ordering.

The five affected live in-review tasks had their stored bases repaired separately (board counts dropped from 21–24 files to their true 2–7). Verified with the full engine suite (6,256 tests) plus the new 4-test real-git suite.


Compound Engineering
Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed "files changed" contamination in in-review tasks following rebase-and-push operations. Base commit detection now prioritizes local branch references over remote ones, preventing incorrect predecessor commits from appearing in file change diffs.
  • Tests

    • Added comprehensive real-git integration tests validating base commit resolution behavior across various git repository states and branch configurations.

In-review tasks showed other tasks' files in their "files changed"
list. Task branches fork from local main, but the base capture
measured merge-base(HEAD, origin/main) — when local main carried
merged-but-unpushed task commits, the recorded base rewound past
them, and after the post-merge rebase-and-push rewrote those SHAs,
baseCommitSha..HEAD permanently swept the predecessors' files into
the new task's diff.

Extract the capture into base-commit-capture.ts, measure local main
first (origin/main fallback) to match the contamination-base sites,
and add a real-git regression suite covering local-ahead-of-origin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@gsxdsm, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 7 minutes and 54 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 38a2cdaa-5500-4237-9c2d-8665e7329d20

📥 Commits

Reviewing files that changed from the base of the PR and between 68e52e3 and c5cf52e.

📒 Files selected for processing (2)
  • CONCEPTS.md
  • docs/solutions/logic-errors/files-changed-inflated-by-origin-first-base-commit.md
📝 Walkthrough

Walkthrough

This PR extracts inline git merge-base logic into a dedicated resolveCapturedBaseCommitSha resolver function with a three-level fallback chain (local main → origin/main → HEAD), integrates it into the executor's captureBaseCommitSha flow, and adds comprehensive real-git integration tests validating the resolver's behavior across multiple scenarios.

Changes

Base Commit SHA Resolution Refactoring

Layer / File(s) Summary
Base commit SHA resolver function
packages/engine/src/base-commit-capture.ts
New async function resolveCapturedBaseCommitSha computes fork-point base SHA by attempting git merge-base against local main with origin/main and HEAD fallbacks; returns undefined only if all invocations fail, with optional warning log on merge-base failure.
Real-git integration tests
packages/engine/src/__tests__/base-commit-capture.real-git.test.ts
Test suite with runtime git availability gate and per-test repo cleanup. Four scenarios validate: local main ahead returns local fork point, branch with own commits returns original fork point, missing local main falls back to origin/main, and missing both references falls back to HEAD.
Executor integration
packages/engine/src/executor.ts
Import resolver and refactor captureBaseCommitSha to delegate merge-base/HEAD logic to resolveCapturedBaseCommitSha(worktreePath, { warn }); explicitly throws when resolver returns no SHA, preserving non-fatal error handling.
Release notes
.changeset/fix-base-commit-sha-local-main.md
Patch release note documenting baseCommitSha capture fix to prevent "files changed" contamination by measuring base against local main first.

Sequence Diagram

sequenceDiagram
  participant Executor as Executor<br/>(captureBaseCommitSha)
  participant Resolver as resolveCapturedBaseCommitSha
  participant Git as git command

  Executor->>Resolver: resolveCapturedBaseCommitSha(worktreePath, logger)
  Resolver->>Git: git merge-base HEAD main
  alt merge-base main succeeds
    Git-->>Resolver: base SHA
  else merge-base main fails
    Resolver->>Git: git merge-base HEAD origin/main
    alt merge-base origin/main succeeds
      Git-->>Resolver: base SHA
    else merge-base origin/main fails
      Resolver->>Git: git rev-parse HEAD
      alt rev-parse succeeds
        Git-->>Resolver: HEAD SHA
      else all fail
        Resolver-->>Resolver: return undefined
      end
    end
  end
  Resolver-->>Executor: resolved SHA or undefined
  alt SHA resolved
    Executor->>Executor: update task.baseCommitSha<br/>emit audit event
  else no SHA
    Executor->>Executor: throw error<br/>catch as non-fatal
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit hops through git's tangled tree,
Chasing the base commit, wild and free—
First local main, then origin's gleam,
HEAD as a fallback, a trusty beam!
No more contamination, the rebase is clean,
The finest base SHA the files have seen! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and clearly summarizes the main change: fixing baseCommitSha capture to prioritize local main over origin/main, which directly addresses the core issue of excessive 'files changed' counts in in-review tasks.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gsxdsm/fileschanged

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a diff-contamination bug where in-review tasks displayed 20–31 "files changed" when they only touched 2–12, because baseCommitSha was captured using merge-base(HEAD, origin/main) while task branches fork from local main — which can be ahead of origin/main by merged-but-unpushed commits. After the post-merge rebase-and-push rewrites those commits' SHAs, the too-old base becomes permanently unrecoverable.

  • Core fix (base-commit-capture.ts): Extracts the base-commit capture logic into a standalone module and swaps the merge-base command to local-main-first (git merge-base HEAD main 2>/dev/null || git merge-base HEAD origin/main), aligning with the two existing contamination-base sites that were already local-first.
  • Executor cleanup (executor.ts): Replaces ~13 lines of inline git invocations with a single call to the new helper; the fallback-to-HEAD path now explicitly throws when even git rev-parse HEAD fails, surfacing the error clearly rather than silently continuing.
  • Real-git regression suite (base-commit-capture.real-git.test.ts): Four integration tests run against actual git repos, including the local-ahead-of-origin scenario that cannot be distinguished by command-string mocks.

Confidence Score: 5/5

Safe to merge — the change is a targeted single-command swap backed by real-git integration tests covering every branch of the fallback chain.

The fix correctly aligns baseCommitSha capture with the two existing contamination-base sites that were already local-main-first. The shell command swap is minimal and the fallback ladder (local main to origin/main to HEAD) is exercised end-to-end by the new real-git test suite. The executor refactor is a straight delegation to the new helper with equivalent error semantics. No logic regressions are visible.

No files require special attention — the new module and its tests are self-contained and the executor change is small.

Important Files Changed

Filename Overview
packages/engine/src/base-commit-capture.ts New module extracting base-commit capture logic; swaps merge-base ordering to local-main-first with origin/main as fallback, and adds a rev-parse HEAD safety net — all paths covered by the new real-git test suite.
packages/engine/src/executor.ts Delegates base-commit capture to the new helper; removes ~13 lines of inline git code; replaces silent fallback-to-HEAD with an explicit throw when the helper returns undefined (only reachable if git rev-parse HEAD itself fails).
packages/engine/src/tests/base-commit-capture.real-git.test.ts Four real-git integration tests covering: the FN-5937 regression (local-ahead-of-origin), normal branch-with-commits case, origin-only fallback, and no-main fallback to HEAD. Skips gracefully when git is unavailable.
CONCEPTS.md Adds four new glossary entries documenting the invariant that all base/fork-point computations must be local-main-first.
docs/solutions/logic-errors/files-changed-inflated-by-origin-first-base-commit.md Comprehensive incident write-up with problem description, symptoms, eliminated hypotheses, root-cause diagram, prevention grep check, and related PRs.
.changeset/fix-base-commit-sha-local-main.md Patch changeset entry describing the fix accurately.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[captureBaseCommitSha called after worktree acquisition] --> B{isResume and task.baseCommitSha?}
    B -- Yes --> C[git merge-base --is-ancestor stored SHA HEAD]
    C -- passes --> D[Preserve stored baseCommitSha]
    C -- stale or fails --> E[Recapture]
    B -- No --> E
    E --> F[resolveCapturedBaseCommitSha]
    F --> G[git merge-base HEAD main or git merge-base HEAD origin/main]
    G -- succeeds --> H[baseCommitSha = SHA]
    G -- both fail --> I[log warn]
    I --> J[git rev-parse HEAD]
    J -- succeeds --> H
    J -- fails --> K[return undefined]
    H --> L{baseCommitSha defined?}
    K --> L
    L -- undefined --> M[throw Error - caught by outer try-catch]
    L -- defined --> N[store.updateTask baseCommitSha]
    style G fill:#d4edda,stroke:#28a745
    style M fill:#fff3cd,stroke:#ffc107
Loading

Reviews (3): Last reviewed commit: "Merge branch 'main' into gsxdsm/filescha..." | Re-trigger Greptile

Comment on lines +23 to +24
* Returns `undefined` only when every git invocation fails (caller treats a
* missing base as non-fatal).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 JSDoc contract contradicts actual caller behaviour

The comment says "caller treats a missing base as non-fatal", but the only real caller — executor.ts — immediately throw new Error("could not resolve base commit SHA") when the return value is undefined. Non-fatal treatment only applies one level further out, in the outer captureBaseCommitSha try-catch. The mismatch could mislead a future caller into expecting the function to be safe to ignore on undefined, when the established usage pattern is to throw.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/engine/src/base-commit-capture.ts (2)

1-4: ⚡ Quick win

Consider adding timeout to exec calls for defensive coding.

While the coding guideline allows execSync for "short deterministic git plumbing" (implying timeouts aren't strictly required), git operations can occasionally hang on corrupted repositories, network-mounted paths, or slow disks. Adding an explicit timeout would prevent indefinite hangs.

⏱️ Example: Add timeout to execAsync wrapper
-const execAsync = promisify(exec);
+const execAsync = promisify(exec);
+const GIT_PLUMBING_TIMEOUT_MS = 10_000; // 10 seconds for git plumbing

Then use { cwd: worktreePath, encoding: "utf-8", timeout: GIT_PLUMBING_TIMEOUT_MS } in the exec options.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/engine/src/base-commit-capture.ts` around lines 1 - 4, Add a default
timeout for git plumbing exec calls by introducing a GIT_PLUMBING_TIMEOUT_MS
constant and replacing plain promisify(exec) usage with a small wrapper (e.g.,
execWithTimeout or change execAsync to accept default options) that merges
provided options with { encoding: "utf-8", timeout: GIT_PLUMBING_TIMEOUT_MS }
before calling exec; update callers in this module that use execAsync to call
the new wrapper so all git exec calls (used in base-commit-capture.ts) will time
out instead of hanging indefinitely.

32-36: ⚡ Quick win

Consider splitting shell OR into JavaScript fallback for better portability and clarity.

The current implementation uses shell-specific syntax (2>/dev/null and ||) that relies on Unix shell behavior. On native Windows (cmd.exe), the 2>/dev/null redirect would fail. While Git Bash typically provides Unix shell semantics, splitting this into separate JavaScript try-catch blocks would be more explicit, portable, and easier to follow.

♻️ Proposed refactor using JavaScript fallback chain
 let baseCommitSha: string | undefined;
 try {
-  const { stdout } = await execAsync(
-    "git merge-base HEAD main 2>/dev/null || git merge-base HEAD origin/main",
-    { cwd: worktreePath, encoding: "utf-8" },
-  );
-  baseCommitSha = stdout.trim() || undefined;
+  // Try local main first
+  try {
+    const { stdout } = await execAsync("git merge-base HEAD main", {
+      cwd: worktreePath,
+      encoding: "utf-8",
+    });
+    baseCommitSha = stdout.trim() || undefined;
+  } catch {
+    // Fall back to origin/main
+    const { stdout } = await execAsync("git merge-base HEAD origin/main", {
+      cwd: worktreePath,
+      encoding: "utf-8",
+    });
+    baseCommitSha = stdout.trim() || undefined;
+  }
 } catch (err: unknown) {
   const errorMessage = err instanceof Error ? err.message : String(err);
   logger?.warn(`merge-base failed, falling back to HEAD: ${errorMessage}`);
 }

This approach makes the fallback chain explicit in JavaScript and works identically across platforms.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/engine/src/base-commit-capture.ts` around lines 32 - 36, The current
execAsync call uses shell-specific redirects and || chaining which breaks on
non-Unix shells; change the logic in base-commit-capture.ts where execAsync is
called (the call that sets baseCommitSha) to run two explicit commands in
JavaScript: first await execAsync("git merge-base HEAD main", { cwd:
worktreePath, encoding: "utf-8" }) in a try/catch, and if that fails or yields
empty stdout, call await execAsync("git merge-base HEAD origin/main", { cwd:
worktreePath, encoding: "utf-8" }); trim the stdout and assign to baseCommitSha
(or undefined) — remove the "2>/dev/null ||" combined-shell usage so behavior is
portable across platforms.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/engine/src/base-commit-capture.ts`:
- Around line 1-4: Add a default timeout for git plumbing exec calls by
introducing a GIT_PLUMBING_TIMEOUT_MS constant and replacing plain
promisify(exec) usage with a small wrapper (e.g., execWithTimeout or change
execAsync to accept default options) that merges provided options with {
encoding: "utf-8", timeout: GIT_PLUMBING_TIMEOUT_MS } before calling exec;
update callers in this module that use execAsync to call the new wrapper so all
git exec calls (used in base-commit-capture.ts) will time out instead of hanging
indefinitely.
- Around line 32-36: The current execAsync call uses shell-specific redirects
and || chaining which breaks on non-Unix shells; change the logic in
base-commit-capture.ts where execAsync is called (the call that sets
baseCommitSha) to run two explicit commands in JavaScript: first await
execAsync("git merge-base HEAD main", { cwd: worktreePath, encoding: "utf-8" })
in a try/catch, and if that fails or yields empty stdout, call await
execAsync("git merge-base HEAD origin/main", { cwd: worktreePath, encoding:
"utf-8" }); trim the stdout and assign to baseCommitSha (or undefined) — remove
the "2>/dev/null ||" combined-shell usage so behavior is portable across
platforms.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a0243ba-4144-4809-a723-d6549e85d795

📥 Commits

Reviewing files that changed from the base of the PR and between 4aa9048 and 68e52e3.

📒 Files selected for processing (4)
  • .changeset/fix-base-commit-sha-local-main.md
  • packages/engine/src/__tests__/base-commit-capture.real-git.test.ts
  • packages/engine/src/base-commit-capture.ts
  • packages/engine/src/executor.ts

gsxdsm and others added 2 commits June 3, 2026 15:32
…lary

Document the FN-5937 files-changed inflation root cause in
docs/solutions/ and add the Branching & diff attribution cluster
(Integration branch, Fork point, Rebase-and-push, Contamination)
to CONCEPTS.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant