feat(git): resolve nested repositories in the other git surfaces
Extract GitView's resolution flow into hooks/useNestedGitDirectory (root probe, discovery, auto-select, stale-selection recovery) so the flow no longer depends on SessionSidebar probing the root first, and reuse it in the pull-request view, walkthrough view, and mobile changes surface — all three now operate on the selected nested repository instead of dead-ending on a non-repo root. Shared pending/failed/unsupported/empty states live in git/NestedRepoResolutionStates; desktop changes inherits the behavior through GitView. Selection stays shared per root, so the picker's pick carries across surfaces.
This commit is contained in:
@@ -2,10 +2,11 @@ import React from 'react';
|
||||
import { Icon } from '@/components/icon/Icon';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||
import { useNestedGitDirectory } from '@/hooks/useNestedGitDirectory';
|
||||
import { useDetectedWorktreeMetadata } from '@/hooks/useDetectedWorktreeRoot';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useSessionWorktreeStore } from '@/sync/session-worktree-store';
|
||||
import { useGitStatus, useGitBranches, useGitStore } from '@/stores/useGitStore';
|
||||
import { useGitStatus, useGitBranches, useGitStore, useIsGitRepo } from '@/stores/useGitStore';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { getRootBranch } from '@/lib/worktrees/worktreeStatus';
|
||||
import { getRuntimeKey } from '@/lib/runtime-switch';
|
||||
@@ -14,6 +15,7 @@ import { useI18n } from '@/lib/i18n';
|
||||
import { ScrollShadow } from '@/components/ui/ScrollShadow';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { PullRequestSection } from './git/PullRequestSection';
|
||||
import { NestedRepoResolutionStates } from './git/NestedRepoResolutionStates';
|
||||
import { deriveBaseBranch } from './git/baseBranch';
|
||||
|
||||
const normalizePath = (value?: string | null): string =>
|
||||
@@ -36,9 +38,16 @@ export const PullRequestView: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const { git } = useRuntimeAPIs();
|
||||
const currentDirectory = useEffectiveDirectory();
|
||||
const status = useGitStatus(currentDirectory ?? null);
|
||||
const branches = useGitBranches(currentDirectory ?? null);
|
||||
const { ensureAll } = useGitStore(useShallow((state) => ({ ensureAll: state.ensureAll })));
|
||||
// When the root is not itself a repository, the pull-request workflow
|
||||
// operates on the resolved nested repository instead.
|
||||
const { rootIsGitRepo, gitDirectory, nestedRepos } = useNestedGitDirectory(currentDirectory ?? null);
|
||||
const status = useGitStatus(gitDirectory ?? null);
|
||||
const branches = useGitBranches(gitDirectory ?? null);
|
||||
const isGitRepo = useIsGitRepo(gitDirectory ?? null);
|
||||
const { ensureAll, ensureNestedRepos } = useGitStore(useShallow((state) => ({
|
||||
ensureAll: state.ensureAll,
|
||||
ensureNestedRepos: state.ensureNestedRepos,
|
||||
})));
|
||||
|
||||
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
||||
const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft);
|
||||
@@ -89,11 +98,11 @@ export const PullRequestView: React.FC = () => {
|
||||
const worktreeMetadata = useDetectedWorktreeMetadata(currentDirectory, storeWorktreeMetadata, status?.current ?? undefined);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentDirectory || !git) {
|
||||
if (!gitDirectory || !git) {
|
||||
return;
|
||||
}
|
||||
void ensureAll(currentDirectory, git);
|
||||
}, [currentDirectory, ensureAll, git]);
|
||||
void ensureAll(gitDirectory, git);
|
||||
}, [gitDirectory, ensureAll, git]);
|
||||
|
||||
const [rootBranchHint, setRootBranchHint] = React.useState<string | null>(null);
|
||||
React.useEffect(() => {
|
||||
@@ -122,52 +131,52 @@ export const PullRequestView: React.FC = () => {
|
||||
}, [authoritativeProjectRoot, worktreeMetadata?.projectDirectory]);
|
||||
|
||||
const [remotes, setRemotes] = React.useState<GitRemote[]>(() =>
|
||||
(currentDirectory ? remotesCacheByDirectory.get(remoteCacheKey(currentDirectory)) : undefined) ?? []
|
||||
(gitDirectory ? remotesCacheByDirectory.get(remoteCacheKey(gitDirectory)) : undefined) ?? []
|
||||
);
|
||||
const [remoteUrl, setRemoteUrl] = React.useState<string | null>(() =>
|
||||
(currentDirectory ? remoteUrlCacheByDirectory.get(remoteCacheKey(currentDirectory)) : undefined) ?? null
|
||||
(gitDirectory ? remoteUrlCacheByDirectory.get(remoteCacheKey(gitDirectory)) : undefined) ?? null
|
||||
);
|
||||
React.useEffect(() => {
|
||||
if (!currentDirectory || !git?.getRemotes) {
|
||||
if (!gitDirectory || !git?.getRemotes) {
|
||||
setRemotes([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setRemotes(remotesCacheByDirectory.get(remoteCacheKey(currentDirectory)) ?? []);
|
||||
setRemotes(remotesCacheByDirectory.get(remoteCacheKey(gitDirectory)) ?? []);
|
||||
let cancelled = false;
|
||||
void git.getRemotes(currentDirectory)
|
||||
void git.getRemotes(gitDirectory)
|
||||
.then((remoteList) => {
|
||||
if (cancelled) return;
|
||||
remotesCacheByDirectory.set(remoteCacheKey(currentDirectory), remoteList ?? []);
|
||||
remotesCacheByDirectory.set(remoteCacheKey(gitDirectory), remoteList ?? []);
|
||||
setRemotes(remoteList ?? []);
|
||||
})
|
||||
.catch(() => { if (!cancelled) setRemotes(remotesCacheByDirectory.get(remoteCacheKey(currentDirectory)) ?? []); });
|
||||
.catch(() => { if (!cancelled) setRemotes(remotesCacheByDirectory.get(remoteCacheKey(gitDirectory)) ?? []); });
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [currentDirectory, git]);
|
||||
}, [gitDirectory, git]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentDirectory || !git?.getRemoteUrl) {
|
||||
if (!gitDirectory || !git?.getRemoteUrl) {
|
||||
setRemoteUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setRemoteUrl(remoteUrlCacheByDirectory.get(remoteCacheKey(currentDirectory)) ?? null);
|
||||
setRemoteUrl(remoteUrlCacheByDirectory.get(remoteCacheKey(gitDirectory)) ?? null);
|
||||
let cancelled = false;
|
||||
void git.getRemoteUrl(currentDirectory)
|
||||
void git.getRemoteUrl(gitDirectory)
|
||||
.then((url) => {
|
||||
if (cancelled) return;
|
||||
remoteUrlCacheByDirectory.set(remoteCacheKey(currentDirectory), url);
|
||||
remoteUrlCacheByDirectory.set(remoteCacheKey(gitDirectory), url);
|
||||
setRemoteUrl(url);
|
||||
})
|
||||
.catch(() => { if (!cancelled) setRemoteUrl(remoteUrlCacheByDirectory.get(remoteCacheKey(currentDirectory)) ?? null); });
|
||||
.catch(() => { if (!cancelled) setRemoteUrl(remoteUrlCacheByDirectory.get(remoteCacheKey(gitDirectory)) ?? null); });
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [currentDirectory, git]);
|
||||
}, [gitDirectory, git]);
|
||||
|
||||
const localBranches = React.useMemo(() => {
|
||||
if (!branches?.all) return [];
|
||||
@@ -240,7 +249,31 @@ export const PullRequestView: React.FC = () => {
|
||||
worktreeMetadata?.createdFromBranch,
|
||||
]);
|
||||
|
||||
if (!currentDirectory || !currentBranch) {
|
||||
if (!currentDirectory) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 p-6 text-center">
|
||||
<Icon name="git-pull-request" className="h-12 w-12 text-muted-foreground/50" />
|
||||
<div className="typography-ui-header text-foreground">{t('gitView.pullRequest.title')}</div>
|
||||
<div className="max-w-sm typography-micro text-muted-foreground">{t('gitView.pullRequest.createHint')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Non-repo root: surface nested-repository resolution (discovering, failed,
|
||||
// unsupported, none found, or settling on the auto-selected repository).
|
||||
if (rootIsGitRepo === false || isGitRepo === false) {
|
||||
return (
|
||||
<NestedRepoResolutionStates
|
||||
rootIsGitRepo={rootIsGitRepo}
|
||||
nestedRepos={nestedRepos}
|
||||
onRetryDiscovery={() => {
|
||||
void ensureNestedRepos(currentDirectory, { force: true });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentBranch) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 p-6 text-center">
|
||||
<Icon name="git-pull-request" className="h-12 w-12 text-muted-foreground/50" />
|
||||
@@ -259,7 +292,7 @@ export const PullRequestView: React.FC = () => {
|
||||
preventOverscroll
|
||||
>
|
||||
<PullRequestSection
|
||||
directory={currentDirectory}
|
||||
directory={gitDirectory ?? currentDirectory}
|
||||
branch={currentBranch}
|
||||
baseBranch={baseBranch}
|
||||
trackingBranch={status?.tracking ?? undefined}
|
||||
|
||||
Reference in New Issue
Block a user