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 @@
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";
4 8
5const branches: GitBranch[] = [9const branches: GitBranch[] = [
6 {10 {
@@ -57,3 +61,69 @@ describe("deriveSyncedLocalBranch", () => { · 50 unmodified lines
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−0
@@ -8,6 +8,31 @@ interface DeriveSyncedLocalBranchInput { · 7 unmodified lines
8 queryBranches: ReadonlyArray<GitBranch> | undefined;8 queryBranches: ReadonlyArray<GitBranch> | undefined;
9}9}
10 10
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 { · 9 unmodified lines
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 { · 14 unmodified lines
31 onComposerFocusRequest?: () => void;35 onComposerFocusRequest?: () => void;
32}36}
33 37
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({ · 18 unmodified lines
63 59
64 const branchesQuery = useQuery(gitBranchesQueryOptions(branchCwd));60 const branchesQuery = useQuery(gitBranchesQueryOptions(branchCwd));
65 61
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();