diff --git a/src/main/services/git/GitService.ts b/src/main/services/git/GitService.ts index 3fc4755e..d2df0186 100644 --- a/src/main/services/git/GitService.ts +++ b/src/main/services/git/GitService.ts @@ -436,10 +436,11 @@ export class GitService { } async checkout(branch: string): Promise { - // Remote branch names are in the format "remotes/origin/branch-name" - // git checkout requires "origin/branch-name" format to properly track remote branches - const normalizedBranch = branch.startsWith('remotes/') ? branch.slice(8) : branch; - await this.git.checkout(normalizedBranch); + if (branch.startsWith('remotes/')) { + await this.checkoutRemoteBranch(this.git, branch); + } else { + await this.git.checkout(branch); + } } async createBranch(name: string, startPoint?: string): Promise { @@ -1118,6 +1119,38 @@ export class GitService { } } + /** + * Checkout a remote branch by extracting the local branch name and creating + * a tracking branch if it does not exist locally. + * @param git - SimpleGit instance (main repo or submodule) + * @param branch - Remote branch name in format "remotes/origin/branch-name" + */ + private async checkoutRemoteBranch(git: SimpleGit, branch: string): Promise { + // "remotes/origin/dev" → remoteBranch="origin/dev", localBranch="dev" + const remoteBranch = branch.slice(8); + const slashIdx = remoteBranch.indexOf('/'); + const localBranch = slashIdx >= 0 ? remoteBranch.slice(slashIdx + 1) : remoteBranch; + + try { + // Prefer switching to existing local branch + await git.checkout(localBranch); + } catch (error) { + // Check if error is due to branch not existing + const msg = error instanceof Error ? error.message : String(error); + if ( + msg.includes('did not match') || + msg.includes('pathspec') || + msg.includes('unknown revision') + ) { + // Local branch does not exist: create it and track the remote + await git.checkout(['-b', localBranch, '--track', remoteBranch]); + } else { + // Re-throw other errors (e.g., uncommitted changes blocking checkout) + throw error; + } + } + } + /** * Fetch 单个子模块 */ @@ -1333,11 +1366,17 @@ export class GitService { } /** - * 切换子模块分支 + * Checkout a branch in a submodule. + * Handles remote tracking refs (remotes/origin/dev) by extracting the local + * branch name and creating a tracking branch when it does not exist locally. */ async checkoutSubmoduleBranch(submodulePath: string, branch: string): Promise { const subGit = this.getSubmoduleGit(submodulePath); - await subGit.checkout(branch); + if (branch.startsWith('remotes/')) { + await this.checkoutRemoteBranch(subGit, branch); + } else { + await subGit.checkout(branch); + } } // Static methods for clone operations diff --git a/src/renderer/components/source-control/SourceControlPanel.tsx b/src/renderer/components/source-control/SourceControlPanel.tsx index 8f624ebc..10ff8ddb 100644 --- a/src/renderer/components/source-control/SourceControlPanel.tsx +++ b/src/renderer/components/source-control/SourceControlPanel.tsx @@ -33,6 +33,7 @@ import { useGitUnstage, } from '@/hooks/useSourceControl'; import { + useCheckoutSubmoduleBranch, useStageSubmodule, useSubmoduleBranches, useSubmoduleChanges, @@ -128,6 +129,7 @@ export function SourceControlPanel({ refetch: refetchBranches, } = useGitBranches(rootPath ?? null); const checkoutMutation = useGitCheckout(); + const checkoutSubmoduleMutation = useCheckoutSubmoduleBranch(); // Submodules const { data: submodules = [] } = useSubmodules(rootPath ?? null); @@ -397,21 +399,42 @@ export function SourceControlPanel({ [rootPath, repositories, pushMutation, queryClient, refetchStatus, refetch, refetchCommits, t] ); - // Branch checkout handler + // Branch checkout handler - handles both main repo and submodule branches const handleBranchCheckout = useCallback( async (repoPath: string, branch: string) => { - if (!repoPath || checkoutMutation.isPending) return; + const isPending = checkoutMutation.isPending || checkoutSubmoduleMutation.isPending; + if (!repoPath || isPending) return; + + // Detect submodule by matching repoPath against the submodule list + const submodule = submodules.find((s) => rootPath && repoPath === joinPath(rootPath, s.path)); try { - await checkoutMutation.mutateAsync({ workdir: repoPath, branch }); - refetch(); - refetchBranches(); - refetchCommits(); - refetchStatus(); + if (submodule && rootPath) { + // Use dedicated submodule mutation so onSuccess invalidates + // ['git', 'submodules', rootPath] — the correct cache key + await checkoutSubmoduleMutation.mutateAsync({ + workdir: rootPath, + submodulePath: submodule.path, + branch, + }); + refetchSubmoduleChanges(); + refetchSubmoduleCommits(); + } else { + await checkoutMutation.mutateAsync({ workdir: repoPath, branch }); + refetch(); + refetchBranches(); + refetchCommits(); + refetchStatus(); + } + + // Normalize branch name for display (remotes/origin/dev → dev) + const displayBranch = branch.startsWith('remotes/') + ? branch.slice(branch.indexOf('/', 8) + 1) + : branch; toastManager.add({ title: t('Branch switched'), - description: t('Branch switched to {{branch}}', { branch }), + description: t('Branch switched to {{branch}}', { branch: displayBranch }), type: 'success', timeout: 3000, }); @@ -424,7 +447,19 @@ export function SourceControlPanel({ }); } }, - [checkoutMutation, refetch, refetchBranches, refetchCommits, refetchStatus, t] + [ + checkoutMutation, + checkoutSubmoduleMutation, + submodules, + rootPath, + refetch, + refetchBranches, + refetchCommits, + refetchStatus, + refetchSubmoduleChanges, + refetchSubmoduleCommits, + t, + ] ); // Flatten infinite query data @@ -810,7 +845,7 @@ export function SourceControlPanel({ onSync={handleSync} onPublish={handlePublish} onCheckout={handleBranchCheckout} - isCheckingOut={checkoutMutation.isPending} + isCheckingOut={checkoutMutation.isPending || checkoutSubmoduleMutation.isPending} /> {/* Changes Section (Collapsible) */} @@ -849,7 +884,9 @@ export function SourceControlPanel({ selectedRepoPath && handleBranchCheckout(selectedRepoPath, branch) } isLoading={currentBranchesLoading} - isCheckingOut={checkoutMutation.isPending} + isCheckingOut={ + checkoutMutation.isPending || checkoutSubmoduleMutation.isPending + } size="xs" />