- 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
Unified · Split
| @@ -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", () => { · 50 unmodified lines | ||
| 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 | +}); | |
| @@ -8,6 +8,31 @@ interface DeriveSyncedLocalBranchInput { · 7 unmodified lines | ||
| 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, |
| @@ -10,7 +10,11 @@ import { · 9 unmodified lines | ||
| 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 { · 14 unmodified lines | ||
| 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({ · 18 unmodified lines | ||
| 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(); |