History

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

Julius Marminge committed 8 months ago

Commit c6268921 parent 3c66cdf3 files · +102 −11Browse files

Unified · Split

3 files
  1. apps/web/src/components/BranchToolbar.logic.test.ts +71 −1
  2. apps/web/src/components/BranchToolbar.logic.ts +25 −0
  3. apps/web/src/components/BranchToolbar.tsx +6 −10
apps/web/src/components/BranchToolbar.logic.test.ts+71−1
@@ -1,6 +1,10 @@
11 import { ThreadId, type GitBranch } from "@t3tools/contracts";
22 import { describe, expect, it } from "vitest";
3−import { deriveSyncedLocalBranch } from "./BranchToolbar.logic";
3+import {
4+ dedupeRemoteBranchesWithLocalMatches,
5+ deriveLocalBranchNameFromRemoteRef,
6+ deriveSyncedLocalBranch,
7+} from "./BranchToolbar.logic";
48
59 const branches: GitBranch[] = [
610 {
@@ -57,3 +61,69 @@ describe("deriveSyncedLocalBranch", () => { · 50 unmodified lines
5761 expect(result).toBeNull();
5862 });
5963 });
64+
65+describe("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+
76+describe("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−0
@@ -8,6 +8,31 @@ interface DeriveSyncedLocalBranchInput { · 7 unmodified lines
88 queryBranches: ReadonlyArray<GitBranch> | undefined;
99 }
1010
11+export 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+
19+export 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+
1136 export function deriveSyncedLocalBranch({
1237 activeThreadId,
1338 activeWorktreePath,
apps/web/src/components/BranchToolbar.tsx+6−10
@@ -10,7 +10,11 @@ import { · 9 unmodified lines
1010 } from "../lib/gitReactQuery";
1111 import { readNativeApi } from "../nativeApi";
1212 import { useStore } from "../store";
13−import { deriveSyncedLocalBranch } from "./BranchToolbar.logic";
13+import {
14+ dedupeRemoteBranchesWithLocalMatches,
15+ deriveLocalBranchNameFromRemoteRef,
16+ deriveSyncedLocalBranch,
17+} from "./BranchToolbar.logic";
1418 import { Button } from "./ui/button";
1519 import {
1620 Combobox,
@@ -31,14 +35,6 @@ interface BranchToolbarProps { · 14 unmodified lines
3135 onComposerFocusRequest?: () => void;
3236 }
3337
34−function 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−
4238 export default function BranchToolbar({
4339 threadId,
4440 envMode,
@@ -63,7 +59,7 @@ export default function BranchToolbar({ · 18 unmodified lines
6359
6460 const branchesQuery = useQuery(gitBranchesQueryOptions(branchCwd));
6561
66− const branches = branchesQuery.data?.branches ?? [];
62+ const branches = dedupeRemoteBranchesWithLocalMatches(branchesQuery.data?.branches ?? []);
6763 const branchNames = branches.map((branch) => branch.name);
6864 const branchByName = new Map(branches.map((branch) => [branch.name, branch]));
6965 const trimmedBranchQuery = branchQuery.trim();