authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-17 13:05:30-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-17 15:12:07-08:00
logafdfe16931d292ed5de2772ccc1afd95120ff6b2
tree451de10e1b889dc4a08b26feedc0e52c1c71949f
parent73fa2d66c79eb1351fbae55a81c61e1f7b88c619

Extract branch sync logic and limit sync to local mode

- 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 cases

3 files changed, 96 insertions(+), 6 deletions(-)

apps/web/src/components/BranchToolbar.logic.test.ts created+56
......@@ -0,0 +1,56 @@
1import type { GitBranch } from "@t3tools/contracts";
2import { describe, expect, it } from "vitest";
3import { deriveSyncedLocalBranch } from "./BranchToolbar.logic";
4
5const 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
20describe("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 @@
1import type { GitBranch } from "@t3tools/contracts";
2
3interface DeriveSyncedLocalBranchInput {
4 activeThreadId: string | undefined;
5 activeWorktreePath: string | null;
6 envMode: "local" | "worktree";
7 activeThreadBranch: string | null;
8 queryBranches: GitBranch[] | undefined;
9}
10
11export 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 {
99 gitCreateBranchAndCheckoutMutationOptions,
1010} from "../lib/gitReactQuery";
1111import { useStore } from "../store";
12import { deriveSyncedLocalBranch } from "./BranchToolbar.logic";
1213import { Button } from "./ui/button";
1314import {
1415 Combobox,
......@@ -86,17 +87,22 @@ export default function BranchToolbar({
8687 // Keep thread branch synced to git current branch for local threads.
8788 const queryBranches = branchesQuery.data?.branches;
8889 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
9399 dispatch({
94100 type: "SET_THREAD_BRANCH",
95101 threadId: activeThreadId,
96 branch: current.name,
102 branch: syncedBranch,
97103 worktreePath: null,
98104 });
99 }, [activeThreadId, activeWorktreePath, activeThreadBranch, queryBranches, dispatch]);
105 }, [activeThreadId, activeWorktreePath, activeThreadBranch, queryBranches, envMode, dispatch]);
100106
101107 useEffect(() => {
102108 if (isBranchMenuOpen) return;