Skip to content

Commit 7fc96b4

Browse files
authored
Merge pull request #2 from dezren39/fix/worktree-create-from-and-switch
fix: create worktree when using --from and auto-create on switch
2 parents 73a0132 + 7ecfa40 commit 7fc96b4

3 files changed

Lines changed: 332 additions & 25 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
/target
22
problems.md
3+
.opencode/
4+
.worktrees/

src/cmd/checkout.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)