diff --git a/src/main/worktree.ts b/src/main/worktree.ts index 78228eb..bdc238c 100644 --- a/src/main/worktree.ts +++ b/src/main/worktree.ts @@ -194,7 +194,7 @@ export function parseBranchLine(line: string): { } export async function getBranchDetails(cwd: string): Promise { - const SEP = '%x00' + const SEP = '%00' const format = [ '%(refname:short)', '%(HEAD)', @@ -205,7 +205,10 @@ export async function getBranchDetails(cwd: string): Promise { '%(subject)' ].join(SEP) - const stdout = await git(['branch', `--format=${format}`, '--sort=-committerdate'], cwd) + const stdout = await git( + ['for-each-ref', `--format=${format}`, '--sort=-committerdate', 'refs/heads/', 'refs/remotes/'], + cwd + ) let worktrees: WorktreeInfo[] = [] try { @@ -226,8 +229,24 @@ export async function getBranchDetails(cwd: string): Promise { .map((line) => parseBranchLine(line)) .filter((obj) => obj !== null) - const branches: BranchDetail[] = await Promise.all( - parsed.map(async (obj) => { + // Separate local and remote entries. Remote refs start with "origin/". + const localEntries: typeof parsed = [] + const remoteEntries: typeof parsed = [] + for (const obj of parsed) { + if (obj.name.startsWith('origin/')) { + // Skip origin/HEAD pointer + if (obj.name === 'origin/HEAD') continue + remoteEntries.push(obj) + } else { + localEntries.push(obj) + } + } + + const localNames = new Set(localEntries.map((e) => e.name)) + + // Build details for local branches + const localBranches: BranchDetail[] = await Promise.all( + localEntries.map(async (obj) => { const name: string = obj.name const worktreePath = wtByBranch.get(name) || '' const isMain = name === mainBranch @@ -250,12 +269,45 @@ export async function getBranchDetails(cwd: string): Promise { worktreePath, aheadCount, dirty, - pr + pr, + remoteOnly: false } }) ) - return branches + // Build details for remote-only branches (no local counterpart) + const remoteBranches: BranchDetail[] = await Promise.all( + remoteEntries + .filter((obj) => { + const shortName = obj.name.replace(/^origin\//, '') + return !localNames.has(shortName) + }) + .map(async (obj) => { + const shortName = obj.name.replace(/^origin\//, '') + const isMain = obj.name === mainBranch + + const aheadCount = isMain ? 0 : await getAheadCount(cwd, obj.name, mainBranch) + + const pr = prStatuses.get(shortName) ?? NO_PR + + return { + name: shortName, + isHead: false, + upstream: obj.name, + gone: false, + lastCommitDate: obj.date || '', + lastCommitRelative: obj.relative || '', + lastCommitSubject: obj.subject || '', + worktreePath: '', + aheadCount, + dirty: false, + pr, + remoteOnly: true + } + }) + ) + + return [...localBranches, ...remoteBranches] } /** List files changed on a branch (committed vs origin/main + uncommitted in worktree) */ diff --git a/src/renderer/src/components/BranchesView.tsx b/src/renderer/src/components/BranchesView.tsx index 150e8fb..eaf749a 100644 --- a/src/renderer/src/components/BranchesView.tsx +++ b/src/renderer/src/components/BranchesView.tsx @@ -28,26 +28,35 @@ export default function BranchesView({ project, onBack }: BranchesViewProps): Re hasWorktree: boolean } | null>(null) - const loadData = useCallback(async () => { - setLoading(true) - setError(null) - try { - const [br, wt] = await Promise.all([ - api.getBranchDetails(project.cwd), - api.listWorktrees(project.cwd) - ]) - setBranches(br) - setWorktrees(wt) - setSelected(new Set()) - } catch (e) { - setError(e instanceof Error ? e.message : 'Failed to load branch data') - } finally { - setLoading(false) - } - }, [project.cwd]) + const loadData = useCallback( + async (signal?: { cancelled: boolean }) => { + setLoading(true) + setError(null) + try { + const [br, wt] = await Promise.all([ + api.getBranchDetails(project.cwd), + api.listWorktrees(project.cwd) + ]) + if (signal?.cancelled) return + setBranches(br) + setWorktrees(wt) + setSelected(new Set()) + } catch (e) { + if (signal?.cancelled) return + setError(e instanceof Error ? e.message : 'Failed to load branch data') + } finally { + if (!signal?.cancelled) setLoading(false) + } + }, + [project.cwd] + ) useEffect(() => { - loadData() + const signal = { cancelled: false } + loadData(signal) + return () => { + signal.cancelled = true + } }, [loadData]) const showAction = useCallback((msg: string) => { @@ -801,6 +810,15 @@ function BranchRow({ )} + {branch.remoteOnly && ( + + remote + + )} + {stale && (