| author | |
| committer | |
| log | 9bb9023009b0c45c66a609c386cd233855043caf |
| tree | 376bff1c6a8a5d9882012fbb79ae35c6207122bc |
| parent | f70cb5de4517e55f62ed2c71e49291d97a7dc927 |
- add `resolveBranchSelectionTarget` to unify checkout cwd/worktree decisions
- reuse existing worktrees when available, including switching back to main repo
- update branch selector to use shared logic and add targeted unit tests3 files changed, 111 insertions(+), 5 deletions(-)
apps/web/src/components/BranchToolbar.logic.test.ts+71| ... | @@ -4,6 +4,7 @@ import { | ... | @@ -4,6 +4,7 @@ import { |
| 4 | filterBranchPickerItems, | 4 | filterBranchPickerItems, |
| 5 | dedupeRemoteBranchesWithLocalMatches, | 5 | dedupeRemoteBranchesWithLocalMatches, |
| 6 | deriveLocalBranchNameFromRemoteRef, | 6 | deriveLocalBranchNameFromRemoteRef, |
| 7 | resolveBranchSelectionTarget, | ||
| 7 | resolveDraftEnvModeAfterBranchChange, | 8 | resolveDraftEnvModeAfterBranchChange, |
| 8 | resolveBranchToolbarValue, | 9 | resolveBranchToolbarValue, |
| 9 | } from "./BranchToolbar.logic"; | 10 | } from "./BranchToolbar.logic"; |
| ... | @@ -223,3 +224,73 @@ describe("filterBranchPickerItems", () => { | ... | @@ -223,3 +224,73 @@ describe("filterBranchPickerItems", () => { |
| 223 | ).toEqual(["feature/488", "__create_new_branch__:48"]); | 224 | ).toEqual(["feature/488", "__create_new_branch__:48"]); |
| 224 | }); | 225 | }); |
| 225 | }); | 226 | }); |
| 227 | |||
| 228 | describe("resolveBranchSelectionTarget", () => { | ||
| 229 | it("reuses an existing secondary worktree for the selected branch", () => { | ||
| 230 | expect( | ||
| 231 | resolveBranchSelectionTarget({ | ||
| 232 | activeProjectCwd: "/repo", | ||
| 233 | activeWorktreePath: "/repo/.t3/worktrees/feature-a", | ||
| 234 | branch: { | ||
| 235 | isDefault: false, | ||
| 236 | worktreePath: "/repo/.t3/worktrees/feature-b", | ||
| 237 | }, | ||
| 238 | }), | ||
| 239 | ).toEqual({ | ||
| 240 | checkoutCwd: "/repo/.t3/worktrees/feature-b", | ||
| 241 | nextWorktreePath: "/repo/.t3/worktrees/feature-b", | ||
| 242 | reuseExistingWorktree: true, | ||
| 243 | }); | ||
| 244 | }); | ||
| 245 | |||
| 246 | it("switches back to the main repo when the branch already lives there", () => { | ||
| 247 | expect( | ||
| 248 | resolveBranchSelectionTarget({ | ||
| 249 | activeProjectCwd: "/repo", | ||
| 250 | activeWorktreePath: "/repo/.t3/worktrees/feature-a", | ||
| 251 | branch: { | ||
| 252 | isDefault: true, | ||
| 253 | worktreePath: "/repo", | ||
| 254 | }, | ||
| 255 | }), | ||
| 256 | ).toEqual({ | ||
| 257 | checkoutCwd: "/repo", | ||
| 258 | nextWorktreePath: null, | ||
| 259 | reuseExistingWorktree: true, | ||
| 260 | }); | ||
| 261 | }); | ||
| 262 | |||
| 263 | it("checks out the default branch in the main repo when leaving a secondary worktree", () => { | ||
| 264 | expect( | ||
| 265 | resolveBranchSelectionTarget({ | ||
| 266 | activeProjectCwd: "/repo", | ||
| 267 | activeWorktreePath: "/repo/.t3/worktrees/feature-a", | ||
| 268 | branch: { | ||
| 269 | isDefault: true, | ||
| 270 | worktreePath: null, | ||
| 271 | }, | ||
| 272 | }), | ||
| 273 | ).toEqual({ | ||
| 274 | checkoutCwd: "/repo", | ||
| 275 | nextWorktreePath: null, | ||
| 276 | reuseExistingWorktree: false, | ||
| 277 | }); | ||
| 278 | }); | ||
| 279 | |||
| 280 | it("keeps checkout in the current worktree for non-default branches", () => { | ||
| 281 | expect( | ||
| 282 | resolveBranchSelectionTarget({ | ||
| 283 | activeProjectCwd: "/repo", | ||
| 284 | activeWorktreePath: "/repo/.t3/worktrees/feature-a", | ||
| 285 | branch: { | ||
| 286 | isDefault: false, | ||
| 287 | worktreePath: null, | ||
| 288 | }, | ||
| 289 | }), | ||
| 290 | ).toEqual({ | ||
| 291 | checkoutCwd: "/repo/.t3/worktrees/feature-a", | ||
| 292 | nextWorktreePath: "/repo/.t3/worktrees/feature-a", | ||
| 293 | reuseExistingWorktree: false, | ||
| 294 | }); | ||
| 295 | }); | ||
| 296 | }); |
apps/web/src/components/BranchToolbar.logic.ts+29| ... | @@ -112,3 +112,32 @@ export function filterBranchPickerItems(input: { | ... | @@ -112,3 +112,32 @@ export function filterBranchPickerItems(input: { |
| 112 | return itemValue.toLowerCase().includes(normalizedQuery); | 112 | return itemValue.toLowerCase().includes(normalizedQuery); |
| 113 | }); | 113 | }); |
| 114 | } | 114 | } |
| 115 | |||
| 116 | export function resolveBranchSelectionTarget(input: { | ||
| 117 | activeProjectCwd: string; | ||
| 118 | activeWorktreePath: string | null; | ||
| 119 | branch: Pick<GitBranch, "isDefault" | "worktreePath">; | ||
| 120 | }): { | ||
| 121 | checkoutCwd: string; | ||
| 122 | nextWorktreePath: string | null; | ||
| 123 | reuseExistingWorktree: boolean; | ||
| 124 | } { | ||
| 125 | const { activeProjectCwd, activeWorktreePath, branch } = input; | ||
| 126 | |||
| 127 | if (branch.worktreePath) { | ||
| 128 | return { | ||
| 129 | checkoutCwd: branch.worktreePath, | ||
| 130 | nextWorktreePath: branch.worktreePath === activeProjectCwd ? null : branch.worktreePath, | ||
| 131 | reuseExistingWorktree: true, | ||
| 132 | }; | ||
| 133 | } | ||
| 134 | |||
| 135 | const nextWorktreePath = | ||
| 136 | activeWorktreePath !== null && branch.isDefault ? null : activeWorktreePath; | ||
| 137 | |||
| 138 | return { | ||
| 139 | checkoutCwd: nextWorktreePath ?? activeProjectCwd, | ||
| 140 | nextWorktreePath, | ||
| 141 | reuseExistingWorktree: false, | ||
| 142 | }; | ||
| 143 | } |
apps/web/src/components/BranchToolbarBranchSelector.tsx+11-5| ... | @@ -27,6 +27,7 @@ import { | ... | @@ -27,6 +27,7 @@ import { |
| 27 | deriveLocalBranchNameFromRemoteRef, | 27 | deriveLocalBranchNameFromRemoteRef, |
| 28 | EnvMode, | 28 | EnvMode, |
| 29 | filterBranchPickerItems, | 29 | filterBranchPickerItems, |
| 30 | resolveBranchSelectionTarget, | ||
| 30 | resolveBranchToolbarValue, | 31 | resolveBranchToolbarValue, |
| 31 | } from "./BranchToolbar.logic"; | 32 | } from "./BranchToolbar.logic"; |
| 32 | import { Button } from "./ui/button"; | 33 | import { Button } from "./ui/button"; |
| ... | @@ -175,10 +176,15 @@ export function BranchToolbarBranchSelector({ | ... | @@ -175,10 +176,15 @@ export function BranchToolbarBranchSelector({ |
| 175 | return; | 176 | return; |
| 176 | } | 177 | } |
| 177 | 178 | ||
| 179 | const selectionTarget = resolveBranchSelectionTarget({ | ||
| 180 | activeProjectCwd, | ||
| 181 | activeWorktreePath, | ||
| 182 | branch, | ||
| 183 | }); | ||
| 184 | |||
| 178 | // If the branch already lives in a worktree, point the thread there. | 185 | // If the branch already lives in a worktree, point the thread there. |
| 179 | if (branch.worktreePath) { | 186 | if (selectionTarget.reuseExistingWorktree) { |
| 180 | const isMainWorktree = branch.worktreePath === activeProjectCwd; | 187 | onSetThreadBranch(branch.name, selectionTarget.nextWorktreePath); |
| 181 | onSetThreadBranch(branch.name, isMainWorktree ? null : branch.worktreePath); | ||
| 182 | setIsBranchMenuOpen(false); | 188 | setIsBranchMenuOpen(false); |
| 183 | onComposerFocusRequest?.(); | 189 | onComposerFocusRequest?.(); |
| 184 | return; | 190 | return; |
| ... | @@ -194,7 +200,7 @@ export function BranchToolbarBranchSelector({ | ... | @@ -194,7 +200,7 @@ export function BranchToolbarBranchSelector({ |
| 194 | runBranchAction(async () => { | 200 | runBranchAction(async () => { |
| 195 | setOptimisticBranch(selectedBranchName); | 201 | setOptimisticBranch(selectedBranchName); |
| 196 | try { | 202 | try { |
| 197 | await api.git.checkout({ cwd: branchCwd, branch: branch.name }); | 203 | await api.git.checkout({ cwd: selectionTarget.checkoutCwd, branch: branch.name }); |
| 198 | await invalidateGitQueries(queryClient); | 204 | await invalidateGitQueries(queryClient); |
| 199 | } catch (error) { | 205 | } catch (error) { |
| 200 | toastManager.add({ | 206 | toastManager.add({ |
| ... | @@ -214,7 +220,7 @@ export function BranchToolbarBranchSelector({ | ... | @@ -214,7 +220,7 @@ export function BranchToolbarBranchSelector({ |
| 214 | } | 220 | } |
| 215 | 221 | ||
| 216 | setOptimisticBranch(nextBranchName); | 222 | setOptimisticBranch(nextBranchName); |
| 217 | onSetThreadBranch(nextBranchName, activeWorktreePath); | 223 | onSetThreadBranch(nextBranchName, selectionTarget.nextWorktreePath); |
| 218 | }); | 224 | }); |
| 219 | }; | 225 | }; |
| 220 | 226 |