Skip to content

feat(repo): add bounded lazy git repository discovery with workspace … - #1034

Open
shauryagangrade wants to merge 22 commits into
crynta:mainfrom
shauryagangrade:shaurya-g-git-graph-commit
Open

feat(repo): add bounded lazy git repository discovery with workspace …#1034
shauryagangrade wants to merge 22 commits into
crynta:mainfrom
shauryagangrade:shaurya-g-git-graph-commit

Conversation

@shauryagangrade

@shauryagangrade shauryagangrade commented Jul 22, 2026

Copy link
Copy Markdown

…picker

What

Adds a new src/modules/repo module that recursively discovers git repositories (root, submodule, nested) within a workspace, capped by depth, result count, and wall-clock time budgets. A dropdown picker in WorkspaceSurface lets users switch between detected repos when multiple are found.

Closes #952

Why

Workspaces often contain multiple git repositories (monorepos, submodules, nested clones). Without discovery, the app only knows about the workspace root and can't distinguish between repos. This feature is the foundation for scoping terminals, editors, and source-control views to a specific repository.

How

  • Bounded lazy walkdiscoverRepositories() runs only when the workspace root changes (not at startup, not on a timer). It recurses up to maxDepth: 3 directories, returns at most maxResults: 20 repos, and hard-aborts after timeoutSecs: 150ms via performance.now() checks before every directory read.

  • .git as directory vs file.git directories are recognized and added without descending into them. .git files (submodule pointers) are read and parsed for gitdir: <path>, with relative paths resolved via @tauri-apps/api/path.join.

  • Symlink containment — Both the workspace root and every candidate repo root are canonicalized via native.canonicalize(). A startsWith(realWorkspaceRoot) check ensures only repos within the workspace boundary are included. A seenRealPaths Set deduplicates repos reachable through multiple symlink paths.

  • Repo picker UI — A DropdownMenu in WorkspaceSurface (top-right) appears only when 2+ repos are detected. Shows repo name, type badge for submodules/nested, and a checkmark for the active selection.

  • React hookuseRepoDiscovery wraps the async call with sequence-number deduplication (stale-result suppression), cancellation on unmount, and selection preservation across re-discoveries.

  • Zero new dependencies — Uses only the existing native Tauri IPC bridge and @tauri-apps/api/path (both already in the project). No fs.watch, no polling, no background threads.

Testing

  • pnpm lint clean
  • pnpm check-types clean
  • pnpm test clean (486/486 tests pass, including 10 new tests for discoverRepositories)
  • Manual smoke-test of the affected feature
  • (If UI) tested in pnpm tauri dev
  • Platforms tested: macOS
  • Shells tested (if relevant): n/a

Unit tests (src/modules/repo/discover.test.ts)

10 tests covering:

  • Empty workspace → []
  • Root repo detection
  • Canonicalize-based deduplication
  • maxResults cap
  • maxDepth cap
  • .git file (submodule pointer) parsing
  • Graceful skip of unreadable directories
  • Hidden directory skipping (.hidden → skipped, .git → processed)
  • Sort order (root first, then alphabetical)
  • canonicalize failure fallback to manual normalization

Screenshots / GIFs

(TODO: add screenshot of the repo picker dropdown in a multi-repo workspace)

Notes for reviewer

  • The 150ms timeout and 20-repo cap are conservative defaults. If workspaces with many repos are common, these may need tuning — they're easy to adjust via DiscoverOptions.
  • getRepoName extracts the basename of the repo root path. For repos with identical directory names (e.g. two lib/ folders), the name alone won't disambiguate — a follow-up could append the relative parent path.
  • The repo picker currently only switches currentRepoRoot state. Wiring it to scope terminal/editor/git-history views to the selected repo is left for a follow-up to keep this PR focused.
  • @tauri-apps/api/path.join returns Promise<string> (async), which is why every join() call is awaited. This is consistent with Tauri v2's webview API.
  • No src-tauri/ changes in this PR.

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features
    • Automatically discovers Git repositories within the current workspace.
    • Adds a repository picker dropdown when multiple repositories are detected.
    • Preserves the selected repository when it still exists after workspace changes.
  • Bug Fixes
    • Improves discovery coverage (submodules via .git files), skips inaccessible folders, prevents duplicates, enforces timeout/depth/max results, and keeps consistent ordering.
  • Tests
    • Adds unit tests for discovery and repository-selection behavior, including async race handling.
  • Chores
    • Adds happy-dom to dev dependencies for test support.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Repository discovery scans workspace directories for Git repositories, tracks selection through a React hook, wires that state through App, and exposes a conditional repository picker in WorkspaceSurface.

Changes

Repository discovery and selection

Layer / File(s) Summary
Repository discovery engine and validation
src/modules/repo/discover.ts, src/modules/repo/discover.test.ts
Adds bounded Git repository discovery with canonicalization, submodule pointer handling, filtering, deterministic ordering, and traversal tests.
Discovery hook and application wiring
src/modules/repo/index.ts, src/modules/repo/useRepoDiscovery.ts, src/modules/repo/useRepoDiscovery.test.ts, src/app/App.tsx, package.json
Exports repository APIs, manages asynchronous discovery and selection, validates stale and failed results, and passes discovery state into WorkspaceSurface; adds happy-dom for hook tests.
Workspace repository picker
src/app/components/WorkspaceSurface.tsx
Adds a conditional dropdown showing detected repositories, marking the current selection, and invoking onRepoChange when selected.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR adds discovery and a picker, but it does not connect the selected repo to the Git History/Source Control view required by #952. Wire currentRepoRoot into the history/source-control data source so the graph actually switches to the selected repository.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows Conventional Commits and clearly describes the repo-discovery change.
Out of Scope Changes check ✅ Passed All changes support repo discovery, selection, or its tests; no unrelated code or dependencies stand out.

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.

@shauryagangrade
shauryagangrade marked this pull request as ready for review July 22, 2026 14:19
@shauryagangrade
shauryagangrade requested a review from crynta as a code owner July 22, 2026 14:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/app/components/WorkspaceSurface.tsx (1)

43-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Inline repo shape duplicates the exported GitRepo type.

Array<{ repoRoot: string; name: string; type: "root" | "submodule" | "nested" }> re-declares GitRepo from src/modules/repo/discover.ts (already re-exported via src/modules/repo/index.ts). Importing it keeps this prop type in sync automatically if the shape evolves.

♻️ Proposed fix
+import type { GitRepo } from "`@/modules/repo`";
...
-  detectedRepos?: Array<{ repoRoot: string; name: string; type: "root" | "submodule" | "nested" }>;
+  detectedRepos?: GitRepo[];
🤖 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 `@src/app/components/WorkspaceSurface.tsx` around lines 43 - 46, Update the
detectedRepos prop in WorkspaceSurface to use the exported GitRepo type from the
repo module instead of an inline object shape, importing the type through the
existing repo index export and preserving the current optional prop behavior.
src/modules/repo/discover.test.ts (1)

86-97: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assertions don't verify repoRoot/name, which would have caught the .git-path bug.

"respects maxResults option" only checks repos.length, and "sorts results with root first, then alphabetically" only checks type and a self-referential name sort — since both zebra and alpha branches currently resolve to the same (buggy) .git name, the alphabetical assertion is vacuously true and wouldn't fail even if names were wrong. Strengthening these to assert concrete repoRoot/name values would surface regressions like the one flagged in discover.ts.

Also applies to: 159-172

🤖 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 `@src/modules/repo/discover.test.ts` around lines 86 - 97, The tests in
“respects maxResults option” and “sorts results with root first, then
alphabetically” need concrete assertions for each returned repository’s repoRoot
and name. Update the expectations to verify the exact discovered paths and
names, including the workspace root and alphabetically ordered repositories,
while retaining the maxResults limit assertion.
🤖 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.

Inline comments:
In `@src/modules/repo/discover.ts`:
- Around line 53-56: Replace the four symlink-containment prefix checks in the
discovery logic, including those around the seen-path checks, with the existing
isWithinRoot(realPath, realWorkspaceRoot) helper. Apply the same boundary-aware
validation to normalizedRoot and realPath at all referenced sites, preserving
the existing seen-path and traversal behavior.
- Around line 109-167: Update both repository-registration branches in the
directory traversal around the .git directory and .git file handling to set
repoRoot to the canonicalized parent working-tree directory dir, not the .git
entry or resolved gitdir storage path. Use that working-tree path for
getRepoName and seenRealPaths checks so the workspace root is not duplicated and
submodules report their checkout directories.

In `@src/modules/repo/useRepoDiscovery.ts`:
- Around line 25-61: Add dedicated tests for the hook containing the discovery
effect, covering stale-result suppression when a newer workspace discovery
completes first, cancellation after unmount, and preservation of the existing
currentRepoRoot when it remains in the refreshed repository list. Also verify
selection falls back appropriately when it no longer exists, while keeping
discoverRepositories itself covered by its existing tests.

---

Nitpick comments:
In `@src/app/components/WorkspaceSurface.tsx`:
- Around line 43-46: Update the detectedRepos prop in WorkspaceSurface to use
the exported GitRepo type from the repo module instead of an inline object
shape, importing the type through the existing repo index export and preserving
the current optional prop behavior.

In `@src/modules/repo/discover.test.ts`:
- Around line 86-97: The tests in “respects maxResults option” and “sorts
results with root first, then alphabetically” need concrete assertions for each
returned repository’s repoRoot and name. Update the expectations to verify the
exact discovered paths and names, including the workspace root and
alphabetically ordered repositories, while retaining the maxResults limit
assertion.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ec6ecbca-51ff-4f16-82c2-21accb2992f4

📥 Commits

Reviewing files that changed from the base of the PR and between 2b2973f and 156ab5c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • src/app/App.tsx
  • src/app/components/WorkspaceSurface.tsx
  • src/modules/repo/discover.test.ts
  • src/modules/repo/discover.ts
  • src/modules/repo/index.ts
  • src/modules/repo/useRepoDiscovery.ts

Comment thread src/modules/repo/discover.ts Outdated
Comment thread src/modules/repo/discover.ts Outdated
Comment thread src/modules/repo/useRepoDiscovery.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@src/modules/repo/useRepoDiscovery.test.ts`:
- Around line 4-7: Replace the current discovery-call-only tests with tests that
import and mount useRepoDiscovery, using controlled deferred
discoverRepositories promises to exercise the real effect. Assert state
transitions for stale-result handling, cleanup, null-root reset, and selection
preservation, including the relevant terminal/workspace discovery invariant, so
the tests fail when the hook implementation regresses.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c03ea180-5fce-40b0-ab62-f59b03341244

📥 Commits

Reviewing files that changed from the base of the PR and between 156ab5c and a815df6.

📒 Files selected for processing (4)
  • src/app/components/WorkspaceSurface.tsx
  • src/modules/repo/discover.test.ts
  • src/modules/repo/discover.ts
  • src/modules/repo/useRepoDiscovery.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/app/components/WorkspaceSurface.tsx
  • src/modules/repo/discover.test.ts
  • src/modules/repo/discover.ts

Comment thread src/modules/repo/useRepoDiscovery.test.ts Outdated
…picker

- Add src/modules/repo module with discoverRepositories() that recursively
  finds git repos (root, submodule, nested) bounded by depth, count, and
  wall-clock time limits
- Handle .git as both directory and file (submodule pointer) with proper
  working-tree repoRoot resolution
- Enforce symlink/workspace containment via canonicalized path checks
- Add useRepoDiscovery hook with race-condition protection and selection
- Wire up repo picker dropdown in WorkspaceSurface (shown for 2+ repos)
- Add tests covering discovery logic and hook state transitions
- Add happy-dom as devDependency for hook tests with real DOM

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@src/modules/repo/useRepoDiscovery.test.ts`:
- Line 34: Remove the em dash characters from the comments in useRepoDiscovery
tests, including the comments near the test harness and the other referenced
locations. Replace them with guideline-compliant punctuation or wording while
preserving the comments’ meaning.
- Around line 234-253: Remove the “does not update state after unmount” test
because its harness cannot observe the hook’s cancellation guard and only
verifies React’s built-in no-op behavior. Keep the existing stale-result test
around the hook’s sequence/cancellation logic as the test that locks the
relevant invariant.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a53e861-edbf-4535-90d1-0c65acaaba9d

📥 Commits

Reviewing files that changed from the base of the PR and between a815df6 and c7d1de8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • package.json
  • src/modules/repo/useRepoDiscovery.test.ts

Comment thread src/modules/repo/useRepoDiscovery.test.ts Outdated
Comment thread src/modules/repo/useRepoDiscovery.test.ts Outdated
…test

- Replace em dash characters with guideline-compliant punctuation in test comments
- Remove "does not update state after unmount" test that only verifies
  React's built-in no-op behavior; the stale-result test already covers
  the hook's cancellation invariant

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (1)
src/app/App.tsx (1)

1270-1272: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not expose a no-op repository picker.

onRepoChange only updates the hook state, while Git consumers still use explorerRoot; selecting a nested repository changes the picker label but not the Git context. Thread currentRepoRoot through the Git consumers, or hide the picker until selection has an effect.

🤖 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 `@src/app/App.tsx` around lines 1270 - 1272, Update the repository-selection
flow around onRepoChange so choosing a detected repository changes the Git
context used by downstream Git consumers, threading currentRepoRoot through the
relevant consumers and replacing their explorerRoot usage where appropriate. If
that integration cannot be completed, hide or disable the repository picker
instead of exposing a selection that only changes its label.
🤖 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.

Outside diff comments:
In `@src/app/App.tsx`:
- Around line 1270-1272: Update the repository-selection flow around
onRepoChange so choosing a detected repository changes the Git context used by
downstream Git consumers, threading currentRepoRoot through the relevant
consumers and replacing their explorerRoot usage where appropriate. If that
integration cannot be completed, hide or disable the repository picker instead
of exposing a selection that only changes its label.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2f502d5e-83bc-49f5-996a-4f500506cc71

📥 Commits

Reviewing files that changed from the base of the PR and between 391f253 and f696270.

📒 Files selected for processing (1)
  • src/app/App.tsx

@crynta

crynta commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Thanks @shauryagangrade for working on this, the direction makes sense! A few blockers before we can merge:

  • native.readDir() hides dot-prefixed entries, so discovery never sees .git in production. The
    tests currently mock behavior the real API cannot provide.

  • The picker only updates local state; it is not wired to Source Control or Git History yet, so
    selecting a repo has no functional effect and does not fully resolve Git graph does not show nested/sub-repository history (submodules, nested repos in monorepo) #952.

  • The frontend traversal can follow symlinks outside the workspace, while startsWith() is not a
    safe path-boundary check.

  • Multiple sequential IPC calls, startup discovery, and the cooperative timeout are concerning
    for performance. This would be safer and faster as one bounded Rust command.

  • Canonicalized host paths may also break WSL handling.

And please remove the accidental package-lock.json since the project uses pnpm :)

The hook structure and tests are a good start. Once these core issues are addressed, I’d be happy
to take another look!

Move git repository discovery from frontend TypeScript (multiple IPC
calls via native.readDir with JS traversal) to a single Rust command
(git_discover_repos) that:

- Reads all directory entries including dot-prefixed (.git visibility)
- Uses Path::starts_with for safe path-component boundary checks
- Skips symlinks to prevent workspace escape
- Authorizes discovered repos for subsequent git operations
- Handles submodules (.git file with gitdir pointer)
- Enforces cooperative timeout (150ms default)

Fixes: multiple sequential IPC calls, unsafe startsWith() path checks,
frontend traversal following symlinks outside workspace, .git entries
hidden by native.readDir(). The picker's selectedRepoRoot was already
wired through useSourceControlContext to Source Control and Git History.
@shauryagangrade

Copy link
Copy Markdown
Author

@crynta Hello! I appreciate you taking out the time to review my PR. I've implemented the changes you mentioned. Have a look at them, and tell me your thoughts!

- App.tsx: use optional chaining instead of null check
- AiChat.tsx: fix hook ordering, remove non-null assertion, stable keys
- AiComposerInput.tsx: fix dependency arrays, add biome-ignore
- AiStatusBarControls.tsx: memoize hasKeyFor with useCallback
- LocalAgentNotificationsBridge.tsx: extract liveStatus to variable
- PlanDiffReview.tsx: stable keys, add biome-ignore
- useWhisperRecording.ts: forEach to for-of, memoize sttOptions, useCallback teardownStream
- composer.tsx: remove ref from deps, wrap attachFileByPath in useCallback
- security.ts: use template literals
- security.test.ts: fix template curly, add biome-ignore comments
- ShortcutsSection.tsx: fix key usage
- Fix useLiteralKeys in stt.ts
- Add biome-ignore for noAssignInExpressions in edit.ts, colorSwatches.ts, markdownExtras.ts
- Fix useExhaustiveDependencies in EditorPane.tsx, GitDiffPane.tsx, useDocument.ts, FileExplorer.tsx
- Use template literals in inlineExtension.ts and prompt.ts
- Rename unused catch variable in inlineExtension.ts
- Add role="tree" and remove tabIndex in FileExplorer.tsx for a11y compliance
- Use optional chaining in FileExplorer.tsx
- Use stable key in ShortcutsSection.tsx
- Add biome-ignore for noAccumulatingSpread in fileIcons.ts and folderIcons.ts
Extract doc.size into a docSize variable to fix two biome lint warnings
about React hook dependencies and a TypeScript error. The original code
used a conditional expression in the dependency array that was both
incomplete (missing doc.size) and unnecessary. The fix extracts the
conditional access into a variable and uses it consistently in both the
dependency array and the effect body.
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.

Git graph does not show nested/sub-repository history (submodules, nested repos in monorepo)

2 participants