@@ -78,7 +78,15 @@ pub(crate) fn switch_to(
7878 ui:: success ( & format ! ( "Switching to `{target}` in worktree `{wt_path}`" ) ) ;
7979 ui:: hint ( & worktree_edit_hint ( wt_path) ) ;
8080 println ! ( "{wt_path}" ) ;
81+ } else if state. is_managed ( target) {
82+ // Managed branch without a worktree — create one and cd into it.
83+ let wt_path = git:: worktree_path ( target) ?;
84+ git:: worktree_add ( & wt_path, target) ?;
85+ ui:: success ( & format ! ( "Created worktree for `{target}` → {wt_path}" ) ) ;
86+ ui:: hint ( & worktree_edit_hint ( & wt_path) ) ;
87+ println ! ( "{wt_path}" ) ;
8188 } else {
89+ // Trunk or unmanaged — plain checkout.
8290 git:: checkout ( target) ?;
8391 ui:: success ( & format ! ( "Switched to `{target}`" ) ) ;
8492 }
@@ -297,4 +305,134 @@ mod tests {
297305 ) ;
298306 assert ! ( !wt_map. contains_key( "detached" ) ) ;
299307 }
308+
309+ #[ test]
310+ fn switch_to_creates_worktree_for_managed_branch_without_one ( ) {
311+ let _guard = take_env_lock ( ) ;
312+ let repo = init_git_repo ( "checkout-auto-worktree" ) ;
313+ let _cwd = CwdGuard :: enter ( & repo) ;
314+
315+ // Set up a managed branch without a worktree.
316+ let parent_head = git:: rev_parse ( "main" ) . expect ( "main head" ) ;
317+ git:: create_branch_at ( "feat/test" , "main" ) . expect ( "create branch" ) ;
318+
319+ let mut state = StackState :: new ( "main" . to_string ( ) ) ;
320+ state. add_branch ( "feat/test" , "main" , & parent_head, None , None ) ;
321+ state. save ( ) . expect ( "save state" ) ;
322+
323+ // Build worktree map — feat/test should NOT be in it yet.
324+ let wt_map = worktree_map ( ) ;
325+ assert ! (
326+ !wt_map. contains_key( "feat/test" ) ,
327+ "feat/test should not be in worktree map before switch"
328+ ) ;
329+
330+ // switch_to should create the worktree.
331+ switch_to ( & state, "feat/test" , & wt_map) . expect ( "switch should succeed" ) ;
332+
333+ // Verify the worktree was created.
334+ let wt_path = git:: worktree_path ( "feat/test" ) . expect ( "worktree path" ) ;
335+ assert ! (
336+ std:: path:: Path :: new( & wt_path) . exists( ) ,
337+ "worktree directory should exist at {wt_path}"
338+ ) ;
339+
340+ // Verify it shows in git worktree list.
341+ let worktrees = git:: worktree_list ( ) . expect ( "worktree list" ) ;
342+ let has_wt = worktrees
343+ . iter ( )
344+ . any ( |wt| wt. branch . as_deref ( ) == Some ( "feat/test" ) ) ;
345+ assert ! ( has_wt, "feat/test should appear in git worktree list" ) ;
346+ }
347+
348+ #[ test]
349+ fn switch_to_trunk_does_plain_checkout ( ) {
350+ let _guard = take_env_lock ( ) ;
351+ let repo = init_git_repo ( "checkout-trunk-plain" ) ;
352+ let _cwd = CwdGuard :: enter ( & repo) ;
353+
354+ // Create a temporary branch to switch away from main.
355+ git:: create_branch ( "temp-branch" ) . expect ( "create temp" ) ;
356+
357+ let state = StackState :: new ( "main" . to_string ( ) ) ;
358+ state. save ( ) . expect ( "save state" ) ;
359+
360+ let wt_map = worktree_map ( ) ;
361+
362+ // Switching to trunk should do a plain checkout, not create a worktree.
363+ switch_to ( & state, "main" , & wt_map) . expect ( "switch to trunk should succeed" ) ;
364+ assert_eq ! (
365+ git:: current_branch( ) . expect( "branch" ) ,
366+ "main" ,
367+ "should be on main after switch"
368+ ) ;
369+ }
370+
371+ #[ test]
372+ fn switch_to_existing_worktree_does_not_create_another ( ) {
373+ let _guard = take_env_lock ( ) ;
374+ let repo = init_git_repo ( "checkout-existing-wt" ) ;
375+ let _cwd = CwdGuard :: enter ( & repo) ;
376+
377+ let parent_head = git:: rev_parse ( "main" ) . expect ( "main head" ) ;
378+ git:: create_branch_at ( "feat/test" , "main" ) . expect ( "create branch" ) ;
379+
380+ let mut state = StackState :: new ( "main" . to_string ( ) ) ;
381+ state. add_branch ( "feat/test" , "main" , & parent_head, None , None ) ;
382+ state. save ( ) . expect ( "save state" ) ;
383+
384+ // Create the worktree manually first.
385+ let wt_path = git:: worktree_path ( "feat/test" ) . expect ( "worktree path" ) ;
386+ git:: worktree_add ( & wt_path, "feat/test" ) . expect ( "manual worktree add" ) ;
387+
388+ // Build worktree map — feat/test SHOULD be in it now.
389+ let wt_map = worktree_map ( ) ;
390+ assert ! (
391+ wt_map. contains_key( "feat/test" ) ,
392+ "feat/test should be in worktree map"
393+ ) ;
394+
395+ // switch_to should succeed and NOT create a second worktree.
396+ switch_to ( & state, "feat/test" , & wt_map) . expect ( "switch should succeed" ) ;
397+
398+ // Only one worktree for feat/test should exist.
399+ let worktrees = git:: worktree_list ( ) . expect ( "worktree list" ) ;
400+ let wt_count = worktrees
401+ . iter ( )
402+ . filter ( |wt| wt. branch . as_deref ( ) == Some ( "feat/test" ) )
403+ . count ( ) ;
404+ assert_eq ! (
405+ wt_count, 1 ,
406+ "should have exactly one worktree for feat/test"
407+ ) ;
408+ }
409+
410+ #[ test]
411+ fn switch_to_unmanaged_branch_falls_through_to_checkout ( ) {
412+ let _guard = take_env_lock ( ) ;
413+ let repo = init_git_repo ( "checkout-unmanaged" ) ;
414+ let _cwd = CwdGuard :: enter ( & repo) ;
415+
416+ git:: create_branch_at ( "scratch" , "main" ) . expect ( "create scratch" ) ;
417+
418+ let state = StackState :: new ( "main" . to_string ( ) ) ;
419+ state. save ( ) . expect ( "save state" ) ;
420+
421+ let wt_map = worktree_map ( ) ;
422+
423+ // Unmanaged branch not in worktree map should do plain checkout.
424+ switch_to ( & state, "scratch" , & wt_map) . expect ( "switch should succeed" ) ;
425+ assert_eq ! (
426+ git:: current_branch( ) . expect( "branch" ) ,
427+ "scratch" ,
428+ "should be on scratch after switch"
429+ ) ;
430+
431+ // No worktree should have been created.
432+ let wt_path = git:: worktree_path ( "scratch" ) . expect ( "worktree path" ) ;
433+ assert ! (
434+ !std:: path:: Path :: new( & wt_path) . exists( ) ,
435+ "no worktree should be created for unmanaged branch"
436+ ) ;
437+ }
300438}
0 commit comments