From 75005cee9cb985c8f9cfba47367a301db041dce8 Mon Sep 17 00:00:00 2001 From: liuweitao Date: Wed, 4 Mar 2026 14:49:22 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(source-control):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20submodule=20=E5=88=86=E6=94=AF=E5=88=87=E6=8D=A2=E5=87=BA?= =?UTF-8?q?=E7=8E=B0=20HEAD=20=E5=8F=8A=E4=B8=8D=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GitService.checkout/checkoutSubmoduleBranch: 切换远程分支时提取本地分支名, 优先切换已有本地分支,不存在则通过 -b --track 创建追踪分支,避免进入 detached HEAD - SourceControlPanel: submodule 分支切换改用 useCheckoutSubmoduleBranch, 使 onSuccess 以 rootPath 为 key 正确失效缓存,切换后立即刷新分支名显示 Co-Authored-By: Claude Sonnet 4.6 --- src/main/services/git/GitService.ts | 37 ++++++++++--- .../source-control/SourceControlPanel.tsx | 52 +++++++++++++++---- 2 files changed, 73 insertions(+), 16 deletions(-) diff --git a/src/main/services/git/GitService.ts b/src/main/services/git/GitService.ts index 3fc4755e..ca6c67c2 100644 --- a/src/main/services/git/GitService.ts +++ b/src/main/services/git/GitService.ts @@ -436,10 +436,21 @@ 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/')) { + // "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 this.git.checkout(localBranch); + } catch { + // Local branch does not exist: create it and track the remote + await this.git.checkout(['-b', localBranch, '--track', remoteBranch]); + } + } else { + await this.git.checkout(branch); + } } async createBranch(name: string, startPoint?: string): Promise { @@ -1333,11 +1344,25 @@ 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/')) { + // "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 { + await subGit.checkout(localBranch); + } catch { + await subGit.checkout(['-b', localBranch, '--track', remoteBranch]); + } + } 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..c4aba26a 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,17 +399,33 @@ 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(); + } toastManager.add({ title: t('Branch switched'), @@ -424,7 +442,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 +840,7 @@ export function SourceControlPanel({ onSync={handleSync} onPublish={handlePublish} onCheckout={handleBranchCheckout} - isCheckingOut={checkoutMutation.isPending} + isCheckingOut={checkoutMutation.isPending || checkoutSubmoduleMutation.isPending} /> {/* Changes Section (Collapsible) */} @@ -849,7 +879,9 @@ export function SourceControlPanel({ selectedRepoPath && handleBranchCheckout(selectedRepoPath, branch) } isLoading={currentBranchesLoading} - isCheckingOut={checkoutMutation.isPending} + isCheckingOut={ + checkoutMutation.isPending || checkoutSubmoduleMutation.isPending + } size="xs" /> From 161af86d030b540a0961251fdf6b8565d5b418e3 Mon Sep 17 00:00:00 2001 From: liuweitao Date: Wed, 4 Mar 2026 18:11:27 +0800 Subject: [PATCH 2/2] =?UTF-8?q?refactor(source-control):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20submodule=20=E5=88=86=E6=94=AF=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=B4=A8=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 提取重复的远程分支处理逻辑为私有方法 checkoutRemoteBranch - 改进错误处理:区分分支不存在和其他错误(如未提交变更),避免掩盖真实异常 - 修复 Toast 提示:显示实际切换到的本地分支名而非原始远程分支名 解决 PR #339 代码审查中提出的 3 个问题 --- src/main/services/git/GitService.ts | 54 ++++++++++++------- .../source-control/SourceControlPanel.tsx | 7 ++- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/main/services/git/GitService.ts b/src/main/services/git/GitService.ts index ca6c67c2..d2df0186 100644 --- a/src/main/services/git/GitService.ts +++ b/src/main/services/git/GitService.ts @@ -437,17 +437,7 @@ export class GitService { async checkout(branch: string): Promise { if (branch.startsWith('remotes/')) { - // "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 this.git.checkout(localBranch); - } catch { - // Local branch does not exist: create it and track the remote - await this.git.checkout(['-b', localBranch, '--track', remoteBranch]); - } + await this.checkoutRemoteBranch(this.git, branch); } else { await this.git.checkout(branch); } @@ -1129,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 单个子模块 */ @@ -1351,15 +1373,7 @@ export class GitService { async checkoutSubmoduleBranch(submodulePath: string, branch: string): Promise { const subGit = this.getSubmoduleGit(submodulePath); if (branch.startsWith('remotes/')) { - // "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 { - await subGit.checkout(localBranch); - } catch { - await subGit.checkout(['-b', localBranch, '--track', remoteBranch]); - } + await this.checkoutRemoteBranch(subGit, branch); } else { await subGit.checkout(branch); } diff --git a/src/renderer/components/source-control/SourceControlPanel.tsx b/src/renderer/components/source-control/SourceControlPanel.tsx index c4aba26a..10ff8ddb 100644 --- a/src/renderer/components/source-control/SourceControlPanel.tsx +++ b/src/renderer/components/source-control/SourceControlPanel.tsx @@ -427,9 +427,14 @@ export function SourceControlPanel({ 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, });