authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-28 20:16:21-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-28 21:56:22-08:00
logc626892adf57042e0a69a6178853cc4e6c8fb6ed
tree0dc792a7c326a97f9bc07abaffa07e0a47969ba1
parent3c66cdf2fe2541b96077a4c6cc48bf60617baf0b

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

3 files changed, 102 insertions(+), 11 deletions(-)

apps/web/src/components/BranchToolbar.logic.test.ts+71-1
...@@ -1,6 +1,10 @@...@@ -1,6 +1,10 @@
1import { ThreadId, type GitBranch } from "@t3tools/contracts";1import { ThreadId, type GitBranch } from "@t3tools/contracts";
2import { describe, expect, it } from "vitest";2import { describe, expect, it } from "vitest";
3import { deriveSyncedLocalBranch } from "./BranchToolbar.logic";3import {
4 dedupeRemoteBranchesWithLocalMatches,
5 deriveLocalBranchNameFromRemoteRef,
6 deriveSyncedLocalBranch,
7} from "./BranchToolbar.logic";
48
5const branches: GitBranch[] = [9const branches: GitBranch[] = [
6 {10 {
...@@ -57,3 +61,69 @@ describe("deriveSyncedLocalBranch", () => {...@@ -57,3 +61,69 @@ describe("deriveSyncedLocalBranch", () => {
57 expect(result).toBeNull();61 expect(result).toBeNull();
58 });62 });
59});63});
64
65describe("deriveLocalBranchNameFromRemoteRef", () => {
66 it("strips the remote prefix from a remote ref", () => {
67 expect(deriveLocalBranchNameFromRemoteRef("origin/feature/demo")).toBe("feature/demo");
68 });
69
70 it("returns the original name when ref is malformed", () => {
71 expect(deriveLocalBranchNameFromRemoteRef("origin/")).toBe("origin/");
72 expect(deriveLocalBranchNameFromRemoteRef("/feature/demo")).toBe("/feature/demo");
73 });
74});
75
76describe("dedupeRemoteBranchesWithLocalMatches", () => {
77 it("hides remote refs when the matching local branch exists", () => {
78 const input: GitBranch[] = [
79 {
80 name: "feature/demo",
81 current: false,
82 isDefault: false,
83 worktreePath: null,
84 },
85 {
86 name: "origin/feature/demo",
87 isRemote: true,
88 current: false,
89 isDefault: false,
90 worktreePath: null,
91 },
92 {
93 name: "origin/feature/remote-only",
94 isRemote: true,
95 current: false,
96 isDefault: false,
97 worktreePath: null,
98 },
99 ];
100
101 expect(dedupeRemoteBranchesWithLocalMatches(input).map((branch) => branch.name)).toEqual([
102 "feature/demo",
103 "origin/feature/remote-only",
104 ]);
105 });
106
107 it("keeps all entries when no local match exists for a remote ref", () => {
108 const input: GitBranch[] = [
109 {
110 name: "feature/local",
111 current: false,
112 isDefault: false,
113 worktreePath: null,
114 },
115 {
116 name: "origin/feature/remote-only",
117 isRemote: true,
118 current: false,
119 isDefault: false,
120 worktreePath: null,
121 },
122 ];
123
124 expect(dedupeRemoteBranchesWithLocalMatches(input).map((branch) => branch.name)).toEqual([
125 "feature/local",
126 "origin/feature/remote-only",
127 ]);
128 });
129});
apps/web/src/components/BranchToolbar.logic.ts+25
...@@ -8,6 +8,31 @@ interface DeriveSyncedLocalBranchInput {...@@ -8,6 +8,31 @@ interface DeriveSyncedLocalBranchInput {
8 queryBranches: ReadonlyArray<GitBranch> | undefined;8 queryBranches: ReadonlyArray<GitBranch> | undefined;
9}9}
1010
11export function deriveLocalBranchNameFromRemoteRef(branchName: string): string {
12 const separatorIndex = branchName.indexOf("/");
13 if (separatorIndex <= 0 || separatorIndex === branchName.length - 1) {
14 return branchName;
15 }
16 return branchName.slice(separatorIndex + 1);
17}
18
19export function dedupeRemoteBranchesWithLocalMatches(
20 branches: ReadonlyArray<GitBranch>,
21): ReadonlyArray<GitBranch> {
22 const localBranchNames = new Set(
23 branches.filter((branch) => !branch.isRemote).map((branch) => branch.name),
24 );
25
26 return branches.filter((branch) => {
27 if (!branch.isRemote) {
28 return true;
29 }
30
31 const localBranchName = deriveLocalBranchNameFromRemoteRef(branch.name);
32 return !localBranchNames.has(localBranchName);
33 });
34}
35
11export function deriveSyncedLocalBranch({36export function deriveSyncedLocalBranch({
12 activeThreadId,37 activeThreadId,
13 activeWorktreePath,38 activeWorktreePath,
apps/web/src/components/BranchToolbar.tsx+6-10
...@@ -10,7 +10,11 @@ import {...@@ -10,7 +10,11 @@ import {
10} from "../lib/gitReactQuery";10} from "../lib/gitReactQuery";
11import { readNativeApi } from "../nativeApi";11import { readNativeApi } from "../nativeApi";
12import { useStore } from "../store";12import { useStore } from "../store";
13import { deriveSyncedLocalBranch } from "./BranchToolbar.logic";13import {
14 dedupeRemoteBranchesWithLocalMatches,
15 deriveLocalBranchNameFromRemoteRef,
16 deriveSyncedLocalBranch,
17} from "./BranchToolbar.logic";
14import { Button } from "./ui/button";18import { Button } from "./ui/button";
15import {19import {
16 Combobox,20 Combobox,
...@@ -31,14 +35,6 @@ interface BranchToolbarProps {...@@ -31,14 +35,6 @@ interface BranchToolbarProps {
31 onComposerFocusRequest?: () => void;35 onComposerFocusRequest?: () => void;
32}36}
3337
34function deriveLocalBranchNameFromRemoteRef(branchName: string): string {
35 const separatorIndex = branchName.indexOf("/");
36 if (separatorIndex <= 0 || separatorIndex === branchName.length - 1) {
37 return branchName;
38 }
39 return branchName.slice(separatorIndex + 1);
40}
41
42export default function BranchToolbar({38export default function BranchToolbar({
43 threadId,39 threadId,
44 envMode,40 envMode,
...@@ -63,7 +59,7 @@ export default function BranchToolbar({...@@ -63,7 +59,7 @@ export default function BranchToolbar({
6359
64 const branchesQuery = useQuery(gitBranchesQueryOptions(branchCwd));60 const branchesQuery = useQuery(gitBranchesQueryOptions(branchCwd));
6561
66 const branches = branchesQuery.data?.branches ?? [];62 const branches = dedupeRemoteBranchesWithLocalMatches(branchesQuery.data?.branches ?? []);
67 const branchNames = branches.map((branch) => branch.name);63 const branchNames = branches.map((branch) => branch.name);
68 const branchByName = new Map(branches.map((branch) => [branch.name, branch]));64 const branchByName = new Map(branches.map((branch) => [branch.name, branch]));
69 const trimmedBranchQuery = branchQuery.trim();65 const trimmedBranchQuery = branchQuery.trim();