From 78de79c9d69bfe163bcbc0849d41ae63d981f86e Mon Sep 17 00:00:00 2001 From: lx <2012065136@qq.com> Date: Sat, 22 Aug 2026 18:13:50 +0800 Subject: [PATCH 1/3] fix: support selecting child git repositories --- src/client/DiffTab.tsx | 2 +- src/client/GitView.tsx | 60 ++++++++++++++++-------- src/client/api.ts | 11 ++++- src/client/state.ts | 4 +- src/git.ts | 101 ++++++++++++++++++++++++++++------------- src/index.ts | 44 +++++++++++------- tests/git.spec.ts | 32 ++++++++++++- 7 files changed, 181 insertions(+), 73 deletions(-) diff --git a/src/client/DiffTab.tsx b/src/client/DiffTab.tsx index 715edb8..8bcddd3 100644 --- a/src/client/DiffTab.tsx +++ b/src/client/DiffTab.tsx @@ -33,7 +33,7 @@ export function DiffTab(props: { sessionId: string; cwd: string | undefined; dif useEffect(() => { let cancelled = false - const scope: SessionScope = { sessionId, cwd } + const scope: SessionScope = { sessionId, cwd, ...(diff.repoRoot !== undefined ? { repoRoot: diff.repoRoot } : {}) } setLoading(true) setError(null) setData(null) diff --git a/src/client/GitView.tsx b/src/client/GitView.tsx index 4075079..11524f5 100644 --- a/src/client/GitView.tsx +++ b/src/client/GitView.tsx @@ -55,6 +55,13 @@ function baseName(path: string): string { return at === -1 ? path : path.slice(at + 1) } +/** Resolve a repo-relative status path for the shared file/editor routes. */ +function absolutePathOf(root: string | undefined, path: string): string { + if (root === undefined || path.startsWith('/') || /^[A-Za-z]:[\\/]/.test(path)) return path + const separator = root.includes('\\') ? '\\' : '/' + return `${root.replace(/[\\/]+$/u, '')}${separator}${path.replace(/[\\/]/gu, separator)}` +} + /** The ref names of one log row's decorations (`HEAD -> main` → `main`), deduped. */ function refNames(refs: string): string[] { return [...new Set( @@ -87,6 +94,7 @@ export function GitView(props: { }) { const { scope, onOpenFile, onOpenDiff } = props const [status, setStatus] = useState(null) + const [repoRoot, setRepoRoot] = useState(undefined) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [branchNames, setBranchNames] = useState([]) @@ -105,17 +113,20 @@ export function GitView(props: { /** The pending destructive action awaiting confirmation. */ const [confirm, setConfirm] = useState(null) + const gitScope: SessionScope = repoRoot === undefined ? scope : { ...scope, repoRoot } + const refresh = useCallback(async (): Promise => { setLoading(true) setError(null) try { const [statusResult, branchResult, logResult] = await Promise.all([ - api.gitStatus(scope), - api.gitBranch(scope).catch(() => ({ current: '', names: [] as string[] })), + api.gitStatus(gitScope), + api.gitBranch(gitScope).catch(() => ({ current: '', names: [] as string[] })), // The first history page only; the rest arrives via "load more". - api.gitLog(scope, LOG_BATCH, 0).catch(() => [] as GitLogEntry[]), + api.gitLog(gitScope, LOG_BATCH, 0).catch(() => [] as GitLogEntry[]), ]) setStatus(statusResult) + if (statusResult.root !== undefined && statusResult.root !== repoRoot) setRepoRoot(statusResult.root) setBranchNames(branchResult.names) setLogEntries(logResult) setLogEnded(logResult.length < LOG_BATCH) @@ -124,7 +135,7 @@ export function GitView(props: { } finally { setLoading(false) } - }, [scope.sessionId, scope.cwd]) + }, [scope.sessionId, scope.cwd, repoRoot]) useEffect(() => { void refresh() }, [refresh]) @@ -133,7 +144,7 @@ export function GitView(props: { if (logLoadingMore || logEnded) return setLogLoadingMore(true) try { - const next = await api.gitLog(scope, LOG_BATCH, logEntries.length) + const next = await api.gitLog(gitScope, LOG_BATCH, logEntries.length) setLogEntries(entries => [...entries, ...next]) if (next.length < LOG_BATCH) setLogEnded(true) } catch (reason) { @@ -149,7 +160,7 @@ export function GitView(props: { id: `diff:w:${staged ? 's' : 'u'}:${entry.path}`, type: 'diff', title: baseName(entry.path), - diff: { kind: 'worktree', path: entry.path, staged, untracked: isUntracked(entry) }, + diff: { kind: 'worktree', path: entry.path, staged, untracked: isUntracked(entry), repoRoot }, }) } @@ -159,15 +170,15 @@ export function GitView(props: { id: `diff:c:${entry.hashFull}`, type: 'diff', title: `${entry.hash} ${entry.subject}`, - diff: { kind: 'commit', hash: entry.hash, hashFull: entry.hashFull, subject: entry.subject }, + diff: { kind: 'commit', hash: entry.hash, hashFull: entry.hashFull, subject: entry.subject, repoRoot }, }) } const stageEntry = async (entry: GitStatusEntry, staged: boolean): Promise => { setBusy(true) try { - if (staged) await api.gitUnstage(scope, entry.path) - else await api.gitStage(scope, entry.path) + if (staged) await api.gitUnstage(gitScope, entry.path) + else await api.gitStage(gitScope, entry.path) await refresh() } finally { setBusy(false) @@ -177,8 +188,8 @@ export function GitView(props: { const stageAll = async (staged: boolean): Promise => { setBusy(true) try { - if (staged) await api.gitUnstage(scope) - else await api.gitStage(scope) + if (staged) await api.gitUnstage(gitScope) + else await api.gitStage(gitScope) await refresh() } finally { setBusy(false) @@ -191,7 +202,7 @@ export function GitView(props: { setBusy(true) setCommitError(null) try { - await api.gitCommit(scope, message) + await api.gitCommit(gitScope, message) setCommitMsg('') await refresh() } catch (reason) { @@ -206,7 +217,7 @@ export function GitView(props: { setBusy(true) setCommitError(null) try { - await api.gitCheckout(scope, branch) + await api.gitCheckout(gitScope, branch) await refresh() } catch (reason) { setCommitError(`${t('checkoutError')}: ${reason instanceof Error ? reason.message : String(reason)}`) @@ -281,6 +292,17 @@ export function GitView(props: { return (
+ {(status?.repositories?.length ?? 0) > 1 && ( + + )}