fix(git): normalize discovered nested repository paths on the client

The server joins discovered repository paths with the platform separator
while every other git directory key in the UI is normalized, so on Windows a
discovered repository never matched its own selection or the root prefix the
picker strips. Parse the route's response at the boundary and normalize each
path. Note in the store docs that worktree bootstrap and session machinery
stay keyed on the project root while a nested repository is selected.
This commit is contained in:
Bohdan Triapitsyn
2026-08-30 10:23:26 +03:00
parent f6ffb0a1fe
commit 93fdfa50d5
2 changed files with 11 additions and 7 deletions
+10 -7
View File
@@ -35,6 +35,7 @@ import type {
RevertCommitResponse,
ResetToCommitResponse,
} from './api/types';
import { normalizePath } from './pathNormalization';
import { runtimeFetch } from './runtime-fetch';
import { getRuntimeUrlResolver } from './runtime-url';
import { getRuntimeKey } from './runtime-switch';
@@ -146,16 +147,18 @@ export async function listGitDirectories(root: string): Promise<string[]> {
if (!response.ok) {
throw new Error(`Failed to list git directories: ${response.statusText}`);
}
const data = await response.json();
if (!data || !Array.isArray(data.repositories)) {
// SAFETY: the route is ours (`GET /api/fs/git-dirs`) and answers this exact
// shape on every 2xx; a malformed body fails the array check below.
const data = await response.json() as { repositories?: Array<{ path?: string | null }> };
if (!Array.isArray(data?.repositories)) {
throw new Error('Unexpected git directories response');
}
// The server joins paths with the platform separator; every other git
// directory key in the UI is normalized, so match that here or a Windows
// repository never equals its own selection or root prefix.
return data.repositories
.map((entry: unknown) => {
const path = entry && typeof entry === 'object' && 'path' in entry ? (entry as { path?: unknown }).path : undefined;
return typeof path === 'string' && path.trim() ? path.trim() : null;
})
.filter((path: string | null): path is string => path !== null);
.map((entry) => normalizePath(entry?.path ?? null))
.filter((path): path is string => path !== null);
}
export async function getGitStatus(directory: string, options?: { mode?: 'light' }): Promise<GitStatus> {
+1
View File
@@ -149,6 +149,7 @@ Important properties:
- `ensureStatus()` and `ensureAll()` are the preferred entry points for consumers
- in-flight dedupe exists for status and `ensureAll()`; status dedupe is scoped to the per-directory status mutation revision, so a refresh requested after a mutation never joins a pre-mutation in-flight request
- nested repository discovery (`nestedReposByRoot`, `nestedRepoSelection`, `ensureNestedRepos`) is per-root state for roots that are not themselves git repositories; discovery failure is a `null` marker (never a valid empty result), a runtime without the discovery route (VS Code) commits an `'unsupported'` marker, and an in-flight discovery whose runtime switched is discarded at commit time instead of repopulating the cleared map. Selections are persisted per runtime + root, and `useEffectiveGitDirectory(root)` resolves the directory git surfaces operate on (`root` when the root is a repository, the selected nested repository otherwise). A selection whose repository fails its probe is dropped and remembered session-only (`staleClearedSelections`) so auto-select does not re-pick it and loop walk+probe; manual picker picks bypass the memory. `hooks/useNestedGitDirectory.ts` owns the resolution flow (root probe, discovery, auto-select, stale-selection recovery) for every consuming surface (Git tab, diff view, pull-request view, walkthrough view, mobile changes), and `git/NestedRepoResolutionStates.tsx` renders the shared pending/failed/unsupported/empty states
- worktree bootstrap polling and session/worktree machinery stay keyed on the project root even while a nested repository is selected; only git data and actions follow the selection
- runtime reset replaces all live entries with that runtime's persisted branch seeds and invalidates old completions
- status, branches, log, identity, repository probes, and prefetch diffs commit through runtime and per-channel generations
- status mutations advance a revision so older refreshes cannot undo optimistic or confirmed index changes