| author | |
| committer | |
| log | c626892adf57042e0a69a6178853cc4e6c8fb6ed |
| tree | 0dc792a7c326a97f9bc07abaffa07e0a47969ba1 |
| parent | 3c66cdf2fe2541b96077a4c6cc48bf60617baf0b |
- 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 behavior3 files changed, 102 insertions(+), 11 deletions(-)
apps/web/src/components/BranchToolbar.logic.test.ts+71-1| ... | ... | @@ -1,6 +1,10 @@ |
| 1 | 1 | import { ThreadId, type GitBranch } from "@t3tools/contracts"; |
| 2 | 2 | import { describe, expect, it } from "vitest"; |
| 3 | import { deriveSyncedLocalBranch } from "./BranchToolbar.logic"; | |
| 3 | import { | |
| 4 | dedupeRemoteBranchesWithLocalMatches, | |
| 5 | deriveLocalBranchNameFromRemoteRef, | |
| 6 | deriveSyncedLocalBranch, | |
| 7 | } from "./BranchToolbar.logic"; | |
| 4 | 8 | |
| 5 | 9 | const branches: GitBranch[] = [ |
| 6 | 10 | { |
| ... | ... | @@ -57,3 +61,69 @@ describe("deriveSyncedLocalBranch", () => { |
| 57 | 61 | expect(result).toBeNull(); |
| 58 | 62 | }); |
| 59 | 63 | }); |
| 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| ... | ... | @@ -8,6 +8,31 @@ interface DeriveSyncedLocalBranchInput { |
| 8 | 8 | queryBranches: ReadonlyArray<GitBranch> | undefined; |
| 9 | 9 | } |
| 10 | 10 | |
| 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 | ||
| 11 | 36 | export function deriveSyncedLocalBranch({ |
| 12 | 37 | activeThreadId, |
| 13 | 38 | activeWorktreePath, |
apps/web/src/components/BranchToolbar.tsx+6-10| ... | ... | @@ -10,7 +10,11 @@ import { |
| 10 | 10 | } from "../lib/gitReactQuery"; |
| 11 | 11 | import { readNativeApi } from "../nativeApi"; |
| 12 | 12 | import { useStore } from "../store"; |
| 13 | import { deriveSyncedLocalBranch } from "./BranchToolbar.logic"; | |
| 13 | import { | |
| 14 | dedupeRemoteBranchesWithLocalMatches, | |
| 15 | deriveLocalBranchNameFromRemoteRef, | |
| 16 | deriveSyncedLocalBranch, | |
| 17 | } from "./BranchToolbar.logic"; | |
| 14 | 18 | import { Button } from "./ui/button"; |
| 15 | 19 | import { |
| 16 | 20 | Combobox, |
| ... | ... | @@ -31,14 +35,6 @@ interface BranchToolbarProps { |
| 31 | 35 | onComposerFocusRequest?: () => void; |
| 32 | 36 | } |
| 33 | 37 | |
| 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 | ||
| 42 | 38 | export default function BranchToolbar({ |
| 43 | 39 | threadId, |
| 44 | 40 | envMode, |
| ... | ... | @@ -63,7 +59,7 @@ export default function BranchToolbar({ |
| 63 | 59 | |
| 64 | 60 | const branchesQuery = useQuery(gitBranchesQueryOptions(branchCwd)); |
| 65 | 61 | |
| 66 | const branches = branchesQuery.data?.branches ?? []; | |
| 62 | const branches = dedupeRemoteBranchesWithLocalMatches(branchesQuery.data?.branches ?? []); | |
| 67 | 63 | const branchNames = branches.map((branch) => branch.name); |
| 68 | 64 | const branchByName = new Map(branches.map((branch) => [branch.name, branch])); |
| 69 | 65 | const trimmedBranchQuery = branchQuery.trim(); |