| author | |
| committer | |
| log | afdfe16931d292ed5de2772ccc1afd95120ff6b2 |
| tree | 451de10e1b889dc4a08b26feedc0e52c1c71949f |
| parent | 73fa2d66c79eb1351fbae55a81c61e1f7b88c619 |
- move branch-sync decision into `BranchToolbar.logic.ts`
- avoid auto-sync when env mode is `worktree` or thread has worktree path
- add unit tests for sync and no-sync cases3 files changed, 96 insertions(+), 6 deletions(-)
apps/web/src/components/BranchToolbar.logic.test.ts created+56| ... | ... | @@ -0,0 +1,56 @@ |
| 1 | import type { GitBranch } from "@t3tools/contracts"; | |
| 2 | import { describe, expect, it } from "vitest"; | |
| 3 | import { deriveSyncedLocalBranch } from "./BranchToolbar.logic"; | |
| 4 | ||
| 5 | const branches: GitBranch[] = [ | |
| 6 | { | |
| 7 | name: "main", | |
| 8 | current: true, | |
| 9 | isDefault: true, | |
| 10 | worktreePath: null, | |
| 11 | }, | |
| 12 | { | |
| 13 | name: "feature/demo", | |
| 14 | current: false, | |
| 15 | isDefault: false, | |
| 16 | worktreePath: null, | |
| 17 | }, | |
| 18 | ]; | |
| 19 | ||
| 20 | describe("deriveSyncedLocalBranch", () => { | |
| 21 | it("syncs to git current branch in local mode", () => { | |
| 22 | const result = deriveSyncedLocalBranch({ | |
| 23 | activeThreadId: "thread-1", | |
| 24 | activeWorktreePath: null, | |
| 25 | envMode: "local", | |
| 26 | activeThreadBranch: "feature/demo", | |
| 27 | queryBranches: branches, | |
| 28 | }); | |
| 29 | ||
| 30 | expect(result).toBe("main"); | |
| 31 | }); | |
| 32 | ||
| 33 | it("does not sync when creating a new worktree", () => { | |
| 34 | const result = deriveSyncedLocalBranch({ | |
| 35 | activeThreadId: "thread-1", | |
| 36 | activeWorktreePath: null, | |
| 37 | envMode: "worktree", | |
| 38 | activeThreadBranch: "feature/demo", | |
| 39 | queryBranches: branches, | |
| 40 | }); | |
| 41 | ||
| 42 | expect(result).toBeNull(); | |
| 43 | }); | |
| 44 | ||
| 45 | it("does not sync when thread already targets a worktree path", () => { | |
| 46 | const result = deriveSyncedLocalBranch({ | |
| 47 | activeThreadId: "thread-1", | |
| 48 | activeWorktreePath: "/tmp/repo/worktrees/feature-demo", | |
| 49 | envMode: "local", | |
| 50 | activeThreadBranch: "feature/demo", | |
| 51 | queryBranches: branches, | |
| 52 | }); | |
| 53 | ||
| 54 | expect(result).toBeNull(); | |
| 55 | }); | |
| 56 | }); |
apps/web/src/components/BranchToolbar.logic.ts created+28| ... | ... | @@ -0,0 +1,28 @@ |
| 1 | import type { GitBranch } from "@t3tools/contracts"; | |
| 2 | ||
| 3 | interface DeriveSyncedLocalBranchInput { | |
| 4 | activeThreadId: string | undefined; | |
| 5 | activeWorktreePath: string | null; | |
| 6 | envMode: "local" | "worktree"; | |
| 7 | activeThreadBranch: string | null; | |
| 8 | queryBranches: GitBranch[] | undefined; | |
| 9 | } | |
| 10 | ||
| 11 | export function deriveSyncedLocalBranch({ | |
| 12 | activeThreadId, | |
| 13 | activeWorktreePath, | |
| 14 | envMode, | |
| 15 | activeThreadBranch, | |
| 16 | queryBranches, | |
| 17 | }: DeriveSyncedLocalBranchInput): string | null { | |
| 18 | if (!activeThreadId || activeWorktreePath || envMode !== "local") { | |
| 19 | return null; | |
| 20 | } | |
| 21 | ||
| 22 | const currentBranch = queryBranches?.find((branch) => branch.current); | |
| 23 | if (!currentBranch || currentBranch.name === activeThreadBranch) { | |
| 24 | return null; | |
| 25 | } | |
| 26 | ||
| 27 | return currentBranch.name; | |
| 28 | } |
apps/web/src/components/BranchToolbar.tsx+12-6| ... | ... | @@ -9,6 +9,7 @@ import { |
| 9 | 9 | gitCreateBranchAndCheckoutMutationOptions, |
| 10 | 10 | } from "../lib/gitReactQuery"; |
| 11 | 11 | import { useStore } from "../store"; |
| 12 | import { deriveSyncedLocalBranch } from "./BranchToolbar.logic"; | |
| 12 | 13 | import { Button } from "./ui/button"; |
| 13 | 14 | import { |
| 14 | 15 | Combobox, |
| ... | ... | @@ -86,17 +87,22 @@ export default function BranchToolbar({ |
| 86 | 87 | // Keep thread branch synced to git current branch for local threads. |
| 87 | 88 | const queryBranches = branchesQuery.data?.branches; |
| 88 | 89 | useEffect(() => { |
| 89 | if (!activeThreadId || activeWorktreePath) return; | |
| 90 | const current = queryBranches?.find((branch) => branch.current); | |
| 91 | if (!current) return; | |
| 92 | if (current.name === activeThreadBranch) return; | |
| 90 | const syncedBranch = deriveSyncedLocalBranch({ | |
| 91 | activeThreadId, | |
| 92 | activeWorktreePath, | |
| 93 | envMode, | |
| 94 | activeThreadBranch, | |
| 95 | queryBranches, | |
| 96 | }); | |
| 97 | if (!syncedBranch) return; | |
| 98 | ||
| 93 | 99 | dispatch({ |
| 94 | 100 | type: "SET_THREAD_BRANCH", |
| 95 | 101 | threadId: activeThreadId, |
| 96 | branch: current.name, | |
| 102 | branch: syncedBranch, | |
| 97 | 103 | worktreePath: null, |
| 98 | 104 | }); |
| 99 | }, [activeThreadId, activeWorktreePath, activeThreadBranch, queryBranches, dispatch]); | |
| 105 | }, [activeThreadId, activeWorktreePath, activeThreadBranch, queryBranches, envMode, dispatch]); | |
| 100 | 106 | |
| 101 | 107 | useEffect(() => { |
| 102 | 108 | if (isBranchMenuOpen) return; |