Skip to content

Support submodule diffs and upstream-base compares in Source Control#6350

Open
lvfen wants to merge 7 commits into
stablyai:mainfrom
lvfen:fix/git-diff-submodule-support
Open

Support submodule diffs and upstream-base compares in Source Control#6350
lvfen wants to merge 7 commits into
stablyai:mainfrom
lvfen:fix/git-diff-submodule-support

Conversation

@lvfen

@lvfen lvfen commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Source Control now supports expanding dirty submodules inline so projects that use git submodules can inspect submodule file diffs directly from the
parent repository. Previously, submodule changes could not be viewed in detail, which made submodule-heavy projects harder to review and iterate on.
This also adds a setting to compare changes against the current branch's upstream base instead of always defaulting to main. For branch-based
development, this keeps the diff focused on local branch work, avoids noisy unrelated changes, and removes the need to reset the compare base every
time the project is opened.
The submodule diff path is lazy-loaded and read-only from the parent worktree, with support across local and SSH runtimes.

Screenshots

  • submodule diff
image
  • upstream-base compares
image

Testing

  • pnpm lint

  • pnpm typecheck

  • pnpm test

  • pnpm build

  • Added or updated high-quality tests that would catch regressions, or explained why tests were not needed

    AI Review Report

    Reviewed with an AI coding agent. Risks checked:

    • SSH submodule status/diff path: older relays may not implement git.submoduleStatus; now degrades to an actionable reconnect message via
      isJsonRpcMethodNotFoundError (mirrors clone()/worktreeIsClean), instead of surfacing a raw JSON-RPC method-not-found.
    • upstream-base compare behavior: confirmed compare base only changes the compare/diff view, not PR/rebase merge targets.
    • Source Control UI flicker: branch-compare summary is no longer cleared while upstream status is still loading (remoteStatus === undefined);
      clearing only happens once upstream is loaded and no base resolves.
    • Settings search/test coverage: catalog keywords now include diff base and source control; the compare-base toggle test asserts the specific
      switch and its updateSettings call instead of scanning full markup.
      Cross-platform: changes are platform-agnostic TypeScript (renderer effects, settings UI, SSH provider RPC forwarding). No
      path/shell/shortcut/Electron platform-specific code was added; behavior is identical on macOS, Linux, and Windows. SSH hosted-runtime path was
      specifically considered (the relay compatibility fix).

    Security Audit

    • Input handling: submodulePath/worktreePath are forwarded to existing relay RPCs unchanged; no new shell construction or path concatenation
      introduced.
    • Command execution: no new git/shell command paths; the submodule change only wraps an existing RPC in error handling.
    • RPC/IPC surface: no new IPC/preload methods exposed; the change is error mapping on an existing call.
    • Auth/secrets/dependencies: no changes.
    • Follow-up: none identified.

    Notes

    No additional platform-specific risks. Compare-base behavior is opt-in via the existing setting and only affects the compare view.

lvfen added 3 commits June 25, 2026 17:10
Dirty submodules now expand inline in Source Control to reveal their
inner changes, with file-level diffs that are read-only from the parent
worktree. Inner status is fetched lazily only when a submodule is
expanded, so status polling never recurses into (possibly nested)
submodules. Adds a submodule-status path across local and SSH runtimes
and git providers.
Adds a global setting (default off) that defaults the Source Control
compare base to the current branch's upstream so the panel prioritizes
local changes instead of the full delta versus the repository default
branch. When the branch has no upstream, the compare view falls back to
working-tree-only. This affects only the compare/diff view; the Pull
Request and rebase merge target are unchanged.
…ion gates

Moves the lazy submodule-expansion state into a useSourceControlSubmoduleStatus
hook and centralizes per-row stage/unstage/discard eligibility into
source-control-entry-actions, shrinking SourceControl.tsx and keeping the
read-only submodule rules consistent across the row UI, bulk actions, and tests.
The hook adds a generation guard so a slow submodule-status response from a
previous worktree (common over SSH) can't write stale status into the current
panel. On the relay side, configured submodule paths are read through a
short-TTL per-instance cache so a burst of diff clicks does not re-read
.gitmodules over the SSH link. Adds tests for the new modules.
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a new sourceControlCompareAgainstUpstream setting, its Git settings UI, and compare-base handling in Source Control. It also adds submodule-aware status and diff handling in the main and relay backends, wires a new git.submoduleStatus API through runtime, preload, and renderer clients, and updates Source Control to lazily expand submodule rows and gate row actions for submodule-internal entries.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.46% 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
Title check ✅ Passed The title is concise and accurately summarizes the two main changes: submodule diffs and upstream-base compares.
Description check ✅ Passed The description includes all required sections, screenshots, testing, AI review, security audit, notes, and cross-platform coverage.
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.

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.

@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.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/main/providers/ssh-git-provider.ts (1)

98-103: 🩺 Stability & Availability | 🔵 Trivial

Add an older-relay guard to getSubmoduleStatus.

The git.submoduleStatus RPC call lacks an isJsonRpcMethodNotFoundError check. If a new client connects to an older hosted relay, the error propagates to the UI. Currently, the consuming hook (useSourceControlSubmoduleStatus) simply displays the raw error message (e.g., "method not found"), which is confusing.

Align this wrapper with existing patterns (e.g., getCloneStatus, getWorktreeIsClean): catch the specific incompatibility error and re-throw a user-friendly message instructing the user to reconnect or update orca on the host.

src/main/providers/ssh-git-provider.ts:98-103

  async getSubmoduleStatus(worktreePath: string, submodulePath: string): Promise<GitStatusResult> {
    return (await this.mux.request('git.submoduleStatus', {
      worktreePath,
      submodulePath
    })) as GitStatusResult
  }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3a9f8c69-001e-4415-8c62-7e80a55e0a84

📥 Commits

Reviewing files that changed from the base of the PR and between 03f6058 and a4cfe12.

📒 Files selected for processing (44)
  • src/main/codex-accounts/runtime-home-service.test.ts
  • src/main/codex-accounts/service.test.ts
  • src/main/git/status.test.ts
  • src/main/git/status.ts
  • src/main/ipc/filesystem.ts
  • src/main/providers/ssh-git-provider.ts
  • src/main/providers/types.ts
  • src/main/runtime/orca-runtime-git.ts
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/rpc/methods/git-params.ts
  • src/main/runtime/rpc/methods/git.ts
  • src/preload/api-types.ts
  • src/preload/index.ts
  • src/relay/git-handler-submodule-ops.test.ts
  • src/relay/git-handler-submodule-ops.ts
  • src/relay/git-handler.test.ts
  • src/relay/git-handler.ts
  • src/renderer/src/components/right-sidebar/SourceControl.compare-summary.test.ts
  • src/renderer/src/components/right-sidebar/SourceControl.open-file-highlight.test.tsx
  • src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx
  • src/renderer/src/components/right-sidebar/SourceControl.tsx
  • src/renderer/src/components/right-sidebar/discard-all-sequence.test.ts
  • src/renderer/src/components/right-sidebar/discard-all-sequence.ts
  • src/renderer/src/components/right-sidebar/source-control-entry-actions.test.ts
  • src/renderer/src/components/right-sidebar/source-control-entry-actions.ts
  • src/renderer/src/components/right-sidebar/source-control-submodule-expansion.test.ts
  • src/renderer/src/components/right-sidebar/source-control-submodule-expansion.ts
  • src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.test.tsx
  • src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.ts
  • src/renderer/src/components/settings/CompareAgainstUpstreamSetting.tsx
  • src/renderer/src/components/settings/GitPane.test.ts
  • src/renderer/src/components/settings/GitPane.tsx
  • src/renderer/src/components/settings/git-search.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/runtime/runtime-git-client.ts
  • src/renderer/src/store/slices/editor.ts
  • src/renderer/src/web/web-preload-api.ts
  • src/shared/constants.ts
  • src/shared/git-status-types.ts
  • src/shared/types.ts

Comment thread src/renderer/src/components/right-sidebar/SourceControl.tsx Outdated
Comment thread src/renderer/src/components/settings/CompareAgainstUpstreamSetting.tsx Outdated
Comment thread src/renderer/src/components/settings/git-search.ts
Comment thread src/renderer/src/components/settings/GitPane.test.ts Outdated
lvfen added 2 commits June 26, 2026 10:06
- Degrade git.submoduleStatus to an actionable reconnect hint when an older
  SSH relay lacks the RPC, mirroring clone()/worktreeIsClean fallbacks.
- Keep the branch-compare summary while upstream status is still loading so
  it no longer flickers when switching worktrees with prefer-upstream on.
- Mark the compare-base switch as type="button" to avoid form submission.
- Add diff base / source control keywords to the Git settings search catalog.
- Assert the compare-base toggle's own switch state and updateSettings call.
…e-support

# Conflicts:
#	src/main/git/status.test.ts
#	src/main/git/status.ts
#	src/relay/git-handler.ts

@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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/renderer/src/components/right-sidebar/SourceControl.tsx (2)

1716-1728: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep list-view selection in sync with injected submodule rows.

Line 1716 renders injected list rows, but list-mode selection still uses pre-injection flatEntries at Line 1741. Expanded submodule child files can render with row handlers but stay absent from selection/range/open-key bookkeeping.

Proposed fix
   const visibleSelectionEntries = useMemo(() => {
     if (sourceControlViewMode === 'list') {
-      return flatEntries
+      const arr: FlatEntry[] = []
+      for (const section of displaySections) {
+        if (collapsedSections.has(section.id)) {
+          continue
+        }
+        for (const row of visibleListRowsBySection[section.id] ?? []) {
+          if (row.type === 'entry') {
+            arr.push({
+              key: `${row.entry.area}::${row.entry.path}`,
+              entry: row.entry,
+              area: row.entry.area
+            })
+          }
+        }
+      }
+      return arr
     }
 
     const arr: FlatEntry[] = []

Also update the memo dependencies:

-    flatEntries,
     sourceControlViewMode,
+    visibleListRowsBySection,
     visibleTreeRowsBySection

4777-4785: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Refresh commit history when the compare base changes.

Line 4683 fetches history with compareBaseRef, but this effect still depends on effectiveBaseRef. If only the upstream compare base changes, the visible history can remain stale until another dependency changes.

Proposed fix
   }, [
     activeWorktreeId,
-    effectiveBaseRef,
+    compareBaseRef,
     isBranchVisible,
     isFolder,
     isGitHistoryExpanded,
src/main/git/status.ts (1)

1069-1074: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use the validated submodule path resolver here, not raw path.join().

matchedSubmodule ultimately comes from .gitmodules, so joining it directly bypasses the new worktree-boundary validation added earlier in this PR. A crafted submodule path can make inner diff reads escape the selected repo. Please route this through the existing resolveSubmoduleWorktreePath(...) guard before reading HEAD or recursing into getDiff().


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 722a0521-426d-423a-bf28-42e89c639f81

📥 Commits

Reviewing files that changed from the base of the PR and between a4cfe12 and 8c4649a.

📒 Files selected for processing (16)
  • src/main/codex-accounts/runtime-home-service.test.ts
  • src/main/codex-accounts/service.test.ts
  • src/main/git/status.test.ts
  • src/main/git/status.ts
  • src/main/providers/ssh-git-provider.test.ts
  • src/main/providers/ssh-git-provider.ts
  • src/main/runtime/orca-runtime.ts
  • src/preload/api-types.ts
  • src/preload/index.ts
  • src/relay/git-handler.test.ts
  • src/relay/git-handler.ts
  • src/renderer/src/components/right-sidebar/SourceControl.compare-summary.test.ts
  • src/renderer/src/components/right-sidebar/SourceControl.tsx
  • src/renderer/src/components/settings/CompareAgainstUpstreamSetting.tsx
  • src/renderer/src/components/settings/GitPane.test.ts
  • src/renderer/src/components/settings/git-search.ts
💤 Files with no reviewable changes (3)
  • src/renderer/src/components/settings/git-search.ts
  • src/renderer/src/components/settings/CompareAgainstUpstreamSetting.tsx
  • src/renderer/src/components/settings/GitPane.test.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/preload/index.ts
  • src/preload/api-types.ts
  • src/main/runtime/orca-runtime.ts
  • src/main/codex-accounts/service.test.ts
  • src/renderer/src/components/right-sidebar/SourceControl.compare-summary.test.ts
  • src/main/codex-accounts/runtime-home-service.test.ts
  • src/main/git/status.test.ts
  • src/relay/git-handler.test.ts
  • src/relay/git-handler.ts

@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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/renderer/src/components/right-sidebar/SourceControl.tsx (2)

1716-1728: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep list-view selection in sync with injected submodule rows.

Line 1716 renders injected list rows, but list-mode selection still uses pre-injection flatEntries at Line 1741. Expanded submodule child files can render with row handlers but stay absent from selection/range/open-key bookkeeping.

Proposed fix
   const visibleSelectionEntries = useMemo(() => {
     if (sourceControlViewMode === 'list') {
-      return flatEntries
+      const arr: FlatEntry[] = []
+      for (const section of displaySections) {
+        if (collapsedSections.has(section.id)) {
+          continue
+        }
+        for (const row of visibleListRowsBySection[section.id] ?? []) {
+          if (row.type === 'entry') {
+            arr.push({
+              key: `${row.entry.area}::${row.entry.path}`,
+              entry: row.entry,
+              area: row.entry.area
+            })
+          }
+        }
+      }
+      return arr
     }
 
     const arr: FlatEntry[] = []

Also update the memo dependencies:

-    flatEntries,
     sourceControlViewMode,
+    visibleListRowsBySection,
     visibleTreeRowsBySection

4777-4785: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Refresh commit history when the compare base changes.

Line 4683 fetches history with compareBaseRef, but this effect still depends on effectiveBaseRef. If only the upstream compare base changes, the visible history can remain stale until another dependency changes.

Proposed fix
   }, [
     activeWorktreeId,
-    effectiveBaseRef,
+    compareBaseRef,
     isBranchVisible,
     isFolder,
     isGitHistoryExpanded,
src/main/git/status.ts (1)

1069-1074: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use the validated submodule path resolver here, not raw path.join().

matchedSubmodule ultimately comes from .gitmodules, so joining it directly bypasses the new worktree-boundary validation added earlier in this PR. A crafted submodule path can make inner diff reads escape the selected repo. Please route this through the existing resolveSubmoduleWorktreePath(...) guard before reading HEAD or recursing into getDiff().


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 722a0521-426d-423a-bf28-42e89c639f81

📥 Commits

Reviewing files that changed from the base of the PR and between a4cfe12 and 8c4649a.

📒 Files selected for processing (16)
  • src/main/codex-accounts/runtime-home-service.test.ts
  • src/main/codex-accounts/service.test.ts
  • src/main/git/status.test.ts
  • src/main/git/status.ts
  • src/main/providers/ssh-git-provider.test.ts
  • src/main/providers/ssh-git-provider.ts
  • src/main/runtime/orca-runtime.ts
  • src/preload/api-types.ts
  • src/preload/index.ts
  • src/relay/git-handler.test.ts
  • src/relay/git-handler.ts
  • src/renderer/src/components/right-sidebar/SourceControl.compare-summary.test.ts
  • src/renderer/src/components/right-sidebar/SourceControl.tsx
  • src/renderer/src/components/settings/CompareAgainstUpstreamSetting.tsx
  • src/renderer/src/components/settings/GitPane.test.ts
  • src/renderer/src/components/settings/git-search.ts
💤 Files with no reviewable changes (3)
  • src/renderer/src/components/settings/git-search.ts
  • src/renderer/src/components/settings/CompareAgainstUpstreamSetting.tsx
  • src/renderer/src/components/settings/GitPane.test.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/preload/index.ts
  • src/preload/api-types.ts
  • src/main/runtime/orca-runtime.ts
  • src/main/codex-accounts/service.test.ts
  • src/renderer/src/components/right-sidebar/SourceControl.compare-summary.test.ts
  • src/main/codex-accounts/runtime-home-service.test.ts
  • src/main/git/status.test.ts
  • src/relay/git-handler.test.ts
  • src/relay/git-handler.ts
🛑 Comments failed to post (2)
src/main/git/status.ts (1)

73-91: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Invalidate statusReadsInFlight on mutations too.

The new getStatus() coalescing can now return stale results after a mutation: if stageFile()/commitChanges() starts while a matching status read is still in flight, the next getStatus() joins that pre-mutation promise because only gitDiffReadDedupe is cleared. statusReadsInFlight needs to be cleared in the same mutation invalidation path.

Suggested direction
+function clearGitReadInvalidationState(): void {
+  gitDiffReadDedupe.clear()
+  statusReadsInFlight.clear()
+}
+
 export async function stageFile(
   worktreePath: string,
   filePath: string,
   options: GitRuntimeOptions = {}
 ): Promise<void> {
-  gitDiffReadDedupe.clear()
+  clearGitReadInvalidationState()
   try {
     await gitExecFileAsync(
       ['add', '--', literalPathspec(filePath)],
       gitOptionsForWorktree(worktreePath, options)
     )
   } finally {
-    gitDiffReadDedupe.clear()
+    clearGitReadInvalidationState()
   }
 }

Also applies to: 1727-1740, 1751-1758, 1814-1838, 1849-1882, 1951-1990, 2015-2028, 2040-2053

src/main/providers/ssh-git-provider.ts (1)

113-130: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make getSubmoduleStatus() clear diff dedupe like getStatus().

getStatus() already clears gitDiffReadDedupe before refreshing repo state so later diff reads can’t join stale in-flight work. The new SSH submodule-status path skips that step, so after a submodule refresh the next diff can still reuse an older pending RPC.

…edback

- Route submodule inner diffs through resolveSubmoduleWorktreePath so a
  crafted .gitmodules path can't escape the selected worktree
- Clear statusReadsInFlight alongside the diff dedupe on git mutations so a
  post-mutation getStatus() can't join a stale in-flight read
- Clear the SSH diff dedupe in getSubmoduleStatus to mirror getStatus
- Derive list-view selection from the submodule-injected rows so expanded
  submodule children are selectable
- Refresh commit history when the upstream compare base changes
@lvfen

lvfen commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! All actionable comments from the latest round have been addressed in commit 6888dcb. Details below.

  1. 🔒 src/main/git/status.ts — Use the validated submodule path resolver instead of raw path.join()

Fixed. loadDiff() now validates the matched submodule before any inner read: as soon as findContainingSubmodule() returns a match, we call
resolveSubmoduleWorktreePath(worktreePath, matchedSubmodule) and reuse that result for both the inner-file branch and the gitlink-pointer branch. The
raw path.join(worktreePath, matchedSubmodule) is gone. Since matchedSubmodule originates from .gitmodules, this guarantees a crafted submodule path
(e.g. ../escape) can no longer make the diff read HEAD or recurse outside the selected worktree. For defense in depth, buildSubmodulePointerDiff()
also now defaults its submoduleWorktreePath parameter to resolveSubmoduleWorktreePath(...), so every caller is protected even if invoked directly.
Added two regression tests in status.test.ts that assert getDiff() rejects with "Access denied" for both an inner-file and a pointer diff when
.gitmodules declares an escaping path.

  1. 🎯 src/main/git/status.ts — Invalidate statusReadsInFlight on mutations too

Fixed. Introduced a single clearGitReadInvalidationState() helper that clears both gitDiffReadDedupe and statusReadsInFlight, and routed every
mutation through it (stageFile, unstageFile, commitChanges, discardChanges, bulkDiscardChanges, bulkStageFiles, bulkUnstageFiles) at both the entry
point and the finally block. This prevents a post-mutation getStatus() from joining a pre-mutation in-flight status read and returning stale entries.
The read path in getStatus() itself is deliberately left clearing only gitDiffReadDedupe, so concurrent identical reads still coalesce. The shared
test-reset hook also uses the helper to keep a single source of truth. Added a status.test.ts test verifying a mutation forces the next getStatus()
to issue a fresh status command rather than reusing the in-flight one.

  1. 🎯 src/main/providers/ssh-git-provider.ts — Make getSubmoduleStatus() clear the diff dedupe like getStatus()

Fixed. getSubmoduleStatus() now calls this.gitDiffReadDedupe.clear() before issuing the RPC, mirroring getStatus(). After a submodule status refresh,
a subsequent diff can no longer reuse an older pending RPC. The existing older-relay isJsonRpcMethodNotFoundError guard is preserved. Added an
ssh-git-provider.test.ts test asserting that a getSubmoduleStatus() between two identical getDiff() calls forces a fresh diff RPC (3 total requests
instead of 2).

  1. 🎯 src/renderer/.../SourceControl.tsx — Keep list-view selection in sync with injected submodule rows

Fixed. In list mode, visibleSelectionEntries now derives from the submodule-injected visibleListRowsBySection instead of the pre-injection
flatEntries. I extracted a small pure helper, collectListSelectionEntries(rows), in source-control-submodule-expansion.ts that maps type: 'entry'
rows to FlatEntry objects (skipping placeholders), and the memo iterates visible sections through it. The now-dead flatEntries memo was removed, and
the memo deps were updated (flatEntries → visibleListRowsBySection). Expanded submodule child files now participate in selection/range/open-key
bookkeeping just like the rows that render. Added unit tests for collectListSelectionEntries covering injected children and placeholder exclusion.

  1. 🎯 src/renderer/.../SourceControl.tsx — Refresh commit history when the compare base changes

Fixed. The history effect that calls refreshGitHistoryRef.current() now depends on compareBaseRef instead of effectiveBaseRef. Since history is
fetched with compareBaseRef, this ensures the visible history refreshes when only the upstream compare base moves (which can happen while
effectiveBaseRef stays put under the prefer-upstream setting).

Verification: pnpm typecheck, pnpm lint, and the affected test suites all pass (status, ssh-git-provider, and the full right-sidebar suite — 985
tests green).

@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.

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/main/git/status.ts (1)

1084-1098: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the correct commit endpoints for staged and compare-against-HEAD inner submodule diffs.

The root pointer diff handles staged and compareAgainstHead, but inner file diffs always compare index || HEAD to the submodule working HEAD. For staged submodule pointer changes, this can show the wrong range; for compare-against-HEAD, it can incorrectly use the index as the left side.

Proposed fix
-      const fromOid =
-        (await readGitlinkOidFromIndex(worktreePath, matchedSubmodule, options)) ||
-        (await readGitlinkOidFromTree(worktreePath, 'HEAD', matchedSubmodule, options))
-      const toOid = await readWorkingSubmoduleHead(submoduleWorktreePath, options)
+      const headOid = await readGitlinkOidFromTree(worktreePath, 'HEAD', matchedSubmodule, options)
+      const indexOid = await readGitlinkOidFromIndex(worktreePath, matchedSubmodule, options)
+      const workingHeadOid = await readWorkingSubmoduleHead(submoduleWorktreePath, options)
+      const fromOid = staged || compareAgainstHead ? headOid : indexOid || headOid
+      const toOid = staged ? indexOid : workingHeadOid
🧹 Nitpick comments (1)
src/renderer/src/components/right-sidebar/SourceControl.tsx (1)

4318-4323: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared unstage eligibility helper for bulk selection.

bulkUnstagePaths currently reimplements only part of the row/action eligibility rule. Switching this to canUnstageStatusEntry(entry.entry) keeps the bulk bar aligned with per-row unstage behavior if more read-only cases are added.

♻️ Proposed refactor
   const bulkUnstagePaths = useMemo(
     () =>
       selectedEntries
-        // Why: submodule-internal rows are read-only from the parent worktree.
-        .filter((entry) => entry.area === 'staged' && !entry.entry.submoduleRoot)
+        .filter((entry) => canUnstageStatusEntry(entry.entry))
         .map((entry) => entry.entry.path),
     [selectedEntries]
   )

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0d92cd5a-b7e4-491d-b7fc-b8b3cba6419d

📥 Commits

Reviewing files that changed from the base of the PR and between 8c4649a and 6888dcb.

📒 Files selected for processing (7)
  • src/main/git/status.test.ts
  • src/main/git/status.ts
  • src/main/providers/ssh-git-provider.test.ts
  • src/main/providers/ssh-git-provider.ts
  • src/renderer/src/components/right-sidebar/SourceControl.tsx
  • src/renderer/src/components/right-sidebar/source-control-submodule-expansion.test.ts
  • src/renderer/src/components/right-sidebar/source-control-submodule-expansion.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/providers/ssh-git-provider.test.ts
  • src/main/providers/ssh-git-provider.ts
  • src/renderer/src/components/right-sidebar/source-control-submodule-expansion.ts
  • src/main/git/status.test.ts

Comment thread src/main/git/status.ts
- Support expanding and diffing staged submodule changes (HEAD vs index) independently of unstaged changes (index vs worktree).
- Track submodule expansion states using a compound key of area and path to prevent conflicts between staged and unstaged listings.
- Update the compare-against-upstream setting to a segmented control for the "Default Compare Base" policy.
- Fall back to the repository default branch when comparing a branch with no upstream, preventing comparison views from unexpectedly disappearing.
@AmethystLiang

AmethystLiang commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

感谢PR!I reviewed and changed some design around the "staged" schema (I directly pushed my commit)

will finish the rest of the review tmr (California time)

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/renderer/src/i18n/locales/en.json (1)

7275-7286: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the missing diffBase catalog key.

src/renderer/src/components/settings/git-search.ts Line 117 now looks up auto.components.settings.git.search.diffBase, but this git.search block never defines it. English falls back to "diff base", yet the key will be missing from catalog sync and other locale files will drift from the rest of the compare-base search terms.

Suggested fix
             "branchUpstream": "branch upstream",
+            "diffBase": "diff base",
             "currentBranch": "current branch",
             "upstream": "upstream",
             "localChanges": "local changes",

Based on learnings, feature PRs should keep new locale keys in catalog parity via pnpm run sync:localization-catalog.

Source: Learnings

src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.ts (1)

55-105: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard same-key fetches against out-of-order responses.

This only rejects replies after a worktree/path switch. Because the effect below refetches every expanded key whenever entries refresh, two requests for the same expansionKey can overlap and an older slower response can overwrite newer child rows after a poll or mutation refresh.

Suggested fix
+  const requestSeqRef = useRef<Record<string, number>>({})
+
   useEffect(() => {
     generationRef.current += 1
+    requestSeqRef.current = {}
     setExpandedSubmoduleKeys(new Set())
     setSubmoduleStatusByKey({})
   }, [activeWorktreeId, worktreePath])
...
       const { area, path: submodulePath } = parsed
       const generation = generationRef.current
+      const requestSeq = (requestSeqRef.current[expansionKey] ?? 0) + 1
+      requestSeqRef.current[expansionKey] = requestSeq
...
-        if (generationRef.current !== generation) {
+        if (
+          generationRef.current !== generation ||
+          requestSeqRef.current[expansionKey] !== requestSeq
+        ) {
           return
         }
...
-        if (generationRef.current !== generation) {
+        if (
+          generationRef.current !== generation ||
+          requestSeqRef.current[expansionKey] !== requestSeq
+        ) {
           return
         }
🧹 Nitpick comments (1)
src/renderer/src/components/settings/CompareAgainstUpstreamSetting.tsx (1)

24-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Share the compare-base metadata from one source.

The title, description, and keywords are duplicated here and in src/renderer/src/components/settings/git-search.ts Lines 73-123. That duplication already caused the search path to drift once; exporting one shared metadata helper/constants would keep the rendered setting and the search catalog in lockstep.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2b662e07-8c52-48ec-b98c-f0cf6d426d0e

📥 Commits

Reviewing files that changed from the base of the PR and between 6888dcb and 336a8ad.

📒 Files selected for processing (36)
  • src/main/git/status-porcelain-parser.test.ts
  • src/main/git/status-porcelain-parser.ts
  • src/main/git/status.test.ts
  • src/main/git/status.ts
  • src/main/ipc/filesystem.ts
  • src/main/providers/ssh-git-provider.test.ts
  • src/main/providers/ssh-git-provider.ts
  • src/main/providers/types.ts
  • src/main/runtime/orca-runtime-git.ts
  • src/main/runtime/rpc/methods/git-params.ts
  • src/main/runtime/rpc/methods/git.test.ts
  • src/main/runtime/rpc/methods/git.ts
  • src/preload/api-types.ts
  • src/preload/index.ts
  • src/relay/git-handler-submodule-ops.ts
  • src/relay/git-handler.test.ts
  • src/relay/git-handler.ts
  • src/relay/git-status-output-parser.test.ts
  • src/relay/git-status-output-parser.ts
  • src/renderer/src/components/right-sidebar/SourceControl.compare-summary.test.ts
  • src/renderer/src/components/right-sidebar/SourceControl.tsx
  • src/renderer/src/components/right-sidebar/source-control-submodule-expansion.test.ts
  • src/renderer/src/components/right-sidebar/source-control-submodule-expansion.ts
  • src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.test.tsx
  • src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.ts
  • src/renderer/src/components/settings/CompareAgainstUpstreamSetting.tsx
  • src/renderer/src/components/settings/GitPane.test.ts
  • src/renderer/src/components/settings/git-search.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/runtime/runtime-git-client.test.ts
  • src/renderer/src/runtime/runtime-git-client.ts
  • src/renderer/src/web/web-preload-api.ts
✅ Files skipped from review due to trivial changes (2)
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/ja.json
🚧 Files skipped from review as they are similar to previous changes (20)
  • src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.test.tsx
  • src/main/runtime/rpc/methods/git-params.ts
  • src/preload/api-types.ts
  • src/main/providers/types.ts
  • src/main/providers/ssh-git-provider.ts
  • src/main/runtime/rpc/methods/git.ts
  • src/renderer/src/runtime/runtime-git-client.ts
  • src/renderer/src/components/right-sidebar/SourceControl.compare-summary.test.ts
  • src/renderer/src/web/web-preload-api.ts
  • src/main/ipc/filesystem.ts
  • src/renderer/src/components/right-sidebar/source-control-submodule-expansion.test.ts
  • src/preload/index.ts
  • src/main/providers/ssh-git-provider.test.ts
  • src/renderer/src/components/right-sidebar/source-control-submodule-expansion.ts
  • src/relay/git-handler.test.ts
  • src/relay/git-handler.ts
  • src/relay/git-handler-submodule-ops.ts
  • src/main/git/status.test.ts
  • src/main/git/status.ts
  • src/renderer/src/components/right-sidebar/SourceControl.tsx

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.

3 participants