Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 45 additions & 6 deletions src/main/services/git/GitService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,10 +436,11 @@ export class GitService {
}

async checkout(branch: string): Promise<void> {
// 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<void> {
Expand Down Expand Up @@ -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<void> {
// "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 单个子模块
*/
Expand Down Expand Up @@ -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<void> {
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
Expand Down
59 changes: 48 additions & 11 deletions src/renderer/components/source-control/SourceControlPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
useGitUnstage,
} from '@/hooks/useSourceControl';
import {
useCheckoutSubmoduleBranch,
useStageSubmodule,
useSubmoduleBranches,
useSubmoduleChanges,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
});
Expand All @@ -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
Expand Down Expand Up @@ -810,7 +845,7 @@ export function SourceControlPanel({
onSync={handleSync}
onPublish={handlePublish}
onCheckout={handleBranchCheckout}
isCheckingOut={checkoutMutation.isPending}
isCheckingOut={checkoutMutation.isPending || checkoutSubmoduleMutation.isPending}
/>

{/* Changes Section (Collapsible) */}
Expand Down Expand Up @@ -849,7 +884,9 @@ export function SourceControlPanel({
selectedRepoPath && handleBranchCheckout(selectedRepoPath, branch)
}
isLoading={currentBranchesLoading}
isCheckingOut={checkoutMutation.isPending}
isCheckingOut={
checkoutMutation.isPending || checkoutSubmoduleMutation.isPending
}
size="xs"
/>
</div>
Expand Down
Loading