From c626892adf57042e0a69a6178853cc4e6c8fb6ed Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 28 Feb 2026 20:16:21 -0800 Subject: [PATCH] Hide duplicate remote branches when local branch exists - Deduplicate branch list by removing remote refs that match an existing local branch - Move remote-ref local-name derivation into shared BranchToolbar logic - Add logic tests for remote-ref parsing and deduplication behavior --- .../components/BranchToolbar.logic.test.ts | 72 ++++++++++++++++++- .../web/src/components/BranchToolbar.logic.ts | 25 +++++++ apps/web/src/components/BranchToolbar.tsx | 16 ++--- 3 files changed, 102 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/BranchToolbar.logic.test.ts b/apps/web/src/components/BranchToolbar.logic.test.ts index 9b72890ce5dfe7611d6b41fb8d30a1ad8a165ca2..f7a12d5ec68e4147baaf27def1f878eaae7736be 100644 --- a/apps/web/src/components/BranchToolbar.logic.test.ts +++ b/apps/web/src/components/BranchToolbar.logic.test.ts @@ -1,6 +1,10 @@ import { ThreadId, type GitBranch } from "@t3tools/contracts"; import { describe, expect, it } from "vitest"; -import { deriveSyncedLocalBranch } from "./BranchToolbar.logic"; +import { + dedupeRemoteBranchesWithLocalMatches, + deriveLocalBranchNameFromRemoteRef, + deriveSyncedLocalBranch, +} from "./BranchToolbar.logic"; const branches: GitBranch[] = [ { @@ -57,3 +61,69 @@ describe("deriveSyncedLocalBranch", () => { expect(result).toBeNull(); }); }); + +describe("deriveLocalBranchNameFromRemoteRef", () => { + it("strips the remote prefix from a remote ref", () => { + expect(deriveLocalBranchNameFromRemoteRef("origin/feature/demo")).toBe("feature/demo"); + }); + + it("returns the original name when ref is malformed", () => { + expect(deriveLocalBranchNameFromRemoteRef("origin/")).toBe("origin/"); + expect(deriveLocalBranchNameFromRemoteRef("/feature/demo")).toBe("/feature/demo"); + }); +}); + +describe("dedupeRemoteBranchesWithLocalMatches", () => { + it("hides remote refs when the matching local branch exists", () => { + const input: GitBranch[] = [ + { + name: "feature/demo", + current: false, + isDefault: false, + worktreePath: null, + }, + { + name: "origin/feature/demo", + isRemote: true, + current: false, + isDefault: false, + worktreePath: null, + }, + { + name: "origin/feature/remote-only", + isRemote: true, + current: false, + isDefault: false, + worktreePath: null, + }, + ]; + + expect(dedupeRemoteBranchesWithLocalMatches(input).map((branch) => branch.name)).toEqual([ + "feature/demo", + "origin/feature/remote-only", + ]); + }); + + it("keeps all entries when no local match exists for a remote ref", () => { + const input: GitBranch[] = [ + { + name: "feature/local", + current: false, + isDefault: false, + worktreePath: null, + }, + { + name: "origin/feature/remote-only", + isRemote: true, + current: false, + isDefault: false, + worktreePath: null, + }, + ]; + + expect(dedupeRemoteBranchesWithLocalMatches(input).map((branch) => branch.name)).toEqual([ + "feature/local", + "origin/feature/remote-only", + ]); + }); +}); diff --git a/apps/web/src/components/BranchToolbar.logic.ts b/apps/web/src/components/BranchToolbar.logic.ts index a3185d0514328b2b2894ff65fcc6a89ee9f02b79..606f1ebd92821fc49ec5d57eadafba3c6a8c0ded 100644 --- a/apps/web/src/components/BranchToolbar.logic.ts +++ b/apps/web/src/components/BranchToolbar.logic.ts @@ -8,6 +8,31 @@ interface DeriveSyncedLocalBranchInput { queryBranches: ReadonlyArray | undefined; } +export function deriveLocalBranchNameFromRemoteRef(branchName: string): string { + const separatorIndex = branchName.indexOf("/"); + if (separatorIndex <= 0 || separatorIndex === branchName.length - 1) { + return branchName; + } + return branchName.slice(separatorIndex + 1); +} + +export function dedupeRemoteBranchesWithLocalMatches( + branches: ReadonlyArray, +): ReadonlyArray { + const localBranchNames = new Set( + branches.filter((branch) => !branch.isRemote).map((branch) => branch.name), + ); + + return branches.filter((branch) => { + if (!branch.isRemote) { + return true; + } + + const localBranchName = deriveLocalBranchNameFromRemoteRef(branch.name); + return !localBranchNames.has(localBranchName); + }); +} + export function deriveSyncedLocalBranch({ activeThreadId, activeWorktreePath, diff --git a/apps/web/src/components/BranchToolbar.tsx b/apps/web/src/components/BranchToolbar.tsx index 2b40187ba55d7aa260502be2058fb2595178deff..09feff6a96c0256e35e864939688eb419c11a99e 100644 --- a/apps/web/src/components/BranchToolbar.tsx +++ b/apps/web/src/components/BranchToolbar.tsx @@ -10,7 +10,11 @@ import { } from "../lib/gitReactQuery"; import { readNativeApi } from "../nativeApi"; import { useStore } from "../store"; -import { deriveSyncedLocalBranch } from "./BranchToolbar.logic"; +import { + dedupeRemoteBranchesWithLocalMatches, + deriveLocalBranchNameFromRemoteRef, + deriveSyncedLocalBranch, +} from "./BranchToolbar.logic"; import { Button } from "./ui/button"; import { Combobox, @@ -31,14 +35,6 @@ interface BranchToolbarProps { onComposerFocusRequest?: () => void; } -function deriveLocalBranchNameFromRemoteRef(branchName: string): string { - const separatorIndex = branchName.indexOf("/"); - if (separatorIndex <= 0 || separatorIndex === branchName.length - 1) { - return branchName; - } - return branchName.slice(separatorIndex + 1); -} - export default function BranchToolbar({ threadId, envMode, @@ -63,7 +59,7 @@ export default function BranchToolbar({ const branchesQuery = useQuery(gitBranchesQueryOptions(branchCwd)); - const branches = branchesQuery.data?.branches ?? []; + const branches = dedupeRemoteBranchesWithLocalMatches(branchesQuery.data?.branches ?? []); const branchNames = branches.map((branch) => branch.name); const branchByName = new Map(branches.map((branch) => [branch.name, branch])); const trimmedBranchQuery = branchQuery.trim(); -- 2.54.0