Skip to content

Commit 42356ed

Browse files
committed
feat: simplify branch selection with smart conflict resolution
1 parent ddee056 commit 42356ed

File tree

8 files changed

+575
-96
lines changed

8 files changed

+575
-96
lines changed

CLAUDE.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ cargo check --all-features
5454
cargo doc --no-deps --open
5555
```
5656

57+
5758
### Installation
5859

5960
```bash
@@ -67,13 +68,25 @@ cargo install --path .
6768
source /path/to/git-workers/shell/gw.sh
6869
```
6970

71+
## Recent Changes
72+
73+
### Branch Option Simplification
74+
- Reduced from 3 options to 2: "Create from current HEAD" and "Select branch (smart mode)"
75+
- Smart mode automatically handles branch conflicts and offers appropriate actions
76+
77+
### Key Methods Added/Modified
78+
- **`get_branch_worktree_map()`**: Maps branch names to worktree names, including main worktree detection
79+
- **`list_all_branches()`**: Returns both local and remote branches (remote without "origin/" prefix)
80+
- **`create_worktree_with_new_branch()`**: Creates worktree with new branch from base branch (supports git-flow style workflows)
81+
7082
## Architecture
7183

7284
### Core Module Structure
7385

7486
```
7587
src/
7688
├── main.rs # CLI entry point and main menu loop
89+
├── lib.rs # Library exports
7790
├── commands.rs # Command implementations for menu items
7891
├── git.rs # Git worktree operations (git2 + process::Command)
7992
├── menu.rs # MenuItem enum and icon definitions
@@ -114,12 +127,14 @@ post-switch = ["echo 'Switched to {{worktree_name}}'"]
114127
```
115128

116129
Template variables:
130+
117131
- `{{worktree_name}}`: The worktree name
118132
- `{{worktree_path}}`: Absolute path to worktree
119133

120134
### Worktree Patterns
121135

122136
First worktree creation offers two options:
137+
123138
1. Same level as repository: `../worktree-name`
124139
2. In subdirectory (recommended): `../repo/worktrees/worktree-name`
125140

@@ -128,13 +143,15 @@ Subsequent worktrees follow the established pattern automatically.
128143
### ESC Key Handling
129144

130145
All interactive prompts support ESC cancellation through custom `input_esc_raw` module:
146+
131147
- `input_esc_raw()` returns `Option<String>` (None on ESC)
132148
- `Select::interact_opt()` for menu selections
133149
- `Confirm::interact_opt()` for confirmations
134150

135151
### Worktree Rename Implementation
136152

137153
Since Git lacks native rename functionality:
154+
138155
1. Move directory with `fs::rename`
139156
2. Update `.git/worktrees/<name>` metadata directory
140157
3. Update gitdir files in both directions
@@ -149,9 +166,11 @@ Since Git lacks native rename functionality:
149166

150167
### Testing Considerations
151168

169+
- Integration tests in `tests/` directory (27 test files)
152170
- Some tests are flaky in parallel execution (marked with `#[ignore]`)
153171
- CI sets `CI=true` environment variable to skip flaky tests
154172
- Run with `--test-threads=1` for reliable results
173+
- Use `--nocapture` to see test output for debugging
155174

156175
### Important Constraints
157176

@@ -161,3 +180,4 @@ Since Git lacks native rename functionality:
161180
- Cannot rename worktrees with detached HEAD
162181
- Shell integration supports Bash/Zsh only
163182
- No Windows support (macOS and Linux only)
183+
- Recent breaking change: CLI arguments removed in favor of menu-only interface

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ Git Workers provides an interactive menu-driven interface. Simply run `gw` and n
5656

5757
- **List worktrees** (``): Display all worktrees with branch, changes, and sync status
5858
- **Search worktrees** (`?`): Fuzzy search through worktree names and branches
59-
- **Create worktree** (`+`): Create a new worktree from existing branch or create a new branch
59+
- **Create worktree** (`+`): Create a new worktree with two options:
60+
- **Create from current HEAD**: Creates a new worktree with a new branch from the current HEAD
61+
- **Select branch (smart mode)**: Choose from local/remote branches with automatic conflict resolution:
62+
- Shows both local and remote branches with usage status
63+
- Automatically handles branch conflicts (offers to create new branch if already in use)
64+
- Remote branches are prefixed with `` for easy identification
6065
- **Delete worktree** (`-`): Delete a single worktree with safety checks
6166
- **Batch delete** (`=`): Select and delete multiple worktrees at once (optionally deletes orphaned branches)
6267
- **Cleanup old worktrees** (`~`): Remove worktrees older than specified days

src/commands.rs

Lines changed: 221 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use indicatif::{ProgressBar, ProgressStyle};
2727
use std::time::Duration;
2828
use unicode_width::UnicodeWidthStr;
2929

30-
use crate::constants::section_header;
30+
use crate::constants::{section_header, GIT_REMOTE_PREFIX, WORKTREES_SUBDIR};
3131
use crate::git::{GitWorktreeManager, WorktreeInfo};
3232
use crate::hooks::{self, HookContext};
3333
use crate::input_esc_raw::{
@@ -524,7 +524,7 @@ fn create_worktree_internal(manager: &GitWorktreeManager) -> Result<bool> {
524524

525525
let options = vec![
526526
format!("Same level as repository (../{name})"),
527-
format!("In subdirectory ({repo_name}/worktrees/{name})"),
527+
format!("In subdirectory ({repo_name}/{}/{name})", WORKTREES_SUBDIR),
528528
];
529529

530530
let selection = match Select::with_theme(&get_theme())
@@ -538,16 +538,16 @@ fn create_worktree_internal(manager: &GitWorktreeManager) -> Result<bool> {
538538
};
539539

540540
match selection {
541-
0 => format!("../{}", name), // Same level
542-
_ => format!("worktrees/{}", name), // Subdirectory pattern
541+
0 => format!("../{}", name), // Same level
542+
_ => format!("{}/{}", WORKTREES_SUBDIR, name), // Subdirectory pattern
543543
}
544544
} else {
545545
name.clone()
546546
};
547547

548548
// Branch handling
549549
println!();
550-
let branch_options = vec!["Create from current HEAD", "Create from existing branch"];
550+
let branch_options = vec!["Create from current HEAD", "Select branch (smart mode)"];
551551

552552
let branch_choice = match Select::with_theme(&get_theme())
553553
.with_prompt("Select branch option")
@@ -558,32 +558,221 @@ fn create_worktree_internal(manager: &GitWorktreeManager) -> Result<bool> {
558558
None => return Ok(false),
559559
};
560560

561-
let branch = if branch_choice == 1 {
562-
// Show existing branches
563-
let branches = manager.list_branches()?;
564-
if branches.is_empty() {
565-
utils::print_warning("No branches found, creating from HEAD");
566-
None
567-
} else {
568-
println!();
569-
match Select::with_theme(&get_theme())
570-
.with_prompt("Select a branch")
571-
.items(&branches)
572-
.interact_opt()?
573-
{
574-
Some(selection) => Some(branches[selection].clone()),
575-
None => return Ok(false),
561+
let (branch, new_branch_name) = match branch_choice {
562+
1 => {
563+
// Select branch (smart mode)
564+
let (local_branches, remote_branches) = manager.list_all_branches()?;
565+
if local_branches.is_empty() && remote_branches.is_empty() {
566+
utils::print_warning("No branches found, creating from HEAD");
567+
(None, None)
568+
} else {
569+
// Get branch to worktree mapping
570+
let branch_worktree_map = manager.get_branch_worktree_map()?;
571+
572+
// Create display items without section headers
573+
let mut branch_items: Vec<String> = Vec::new();
574+
let mut branch_refs: Vec<(String, bool)> = Vec::new(); // (branch_name, is_remote)
575+
576+
// Add local branches
577+
for branch in &local_branches {
578+
if let Some(worktree) = branch_worktree_map.get(branch) {
579+
branch_items.push(format!(
580+
" {} {}",
581+
branch.white(),
582+
format!("(in use by '{}')", worktree).bright_red()
583+
));
584+
} else {
585+
branch_items.push(format!(" {}", branch.white()));
586+
}
587+
branch_refs.push((branch.clone(), false));
588+
}
589+
590+
// Add remote branches with clear distinction
591+
for branch in &remote_branches {
592+
let full_remote_name = format!("{}{}", GIT_REMOTE_PREFIX, branch);
593+
if let Some(worktree) = branch_worktree_map.get(&full_remote_name) {
594+
branch_items.push(format!(
595+
"↑ {} {}",
596+
full_remote_name.bright_blue(),
597+
format!("(in use by '{}')", worktree).bright_red()
598+
));
599+
} else {
600+
branch_items.push(format!("↑ {}", full_remote_name.bright_blue()));
601+
}
602+
branch_refs.push((branch.clone(), true));
603+
}
604+
605+
println!();
606+
match Select::with_theme(&get_theme())
607+
.with_prompt("Select a branch")
608+
.items(&branch_items)
609+
.interact_opt()?
610+
{
611+
Some(selection) => {
612+
let (selected_branch, is_remote) = &branch_refs[selection];
613+
614+
if !is_remote {
615+
// Local branch - check if already checked out
616+
if let Some(worktree) = branch_worktree_map.get(selected_branch) {
617+
// Branch is in use, offer to create a new branch
618+
println!();
619+
utils::print_warning(&format!(
620+
"Branch '{}' is already checked out in worktree '{}'",
621+
selected_branch.yellow(),
622+
worktree.bright_red()
623+
));
624+
println!();
625+
626+
let action_options = vec![
627+
format!(
628+
"Create new branch '{}' from '{}'",
629+
name, selected_branch
630+
),
631+
"Change the branch name".to_string(),
632+
"Cancel".to_string(),
633+
];
634+
635+
match Select::with_theme(&get_theme())
636+
.with_prompt("What would you like to do?")
637+
.items(&action_options)
638+
.interact_opt()?
639+
{
640+
Some(0) => {
641+
// Use worktree name as new branch name
642+
(Some(selected_branch.clone()), Some(name.clone()))
643+
}
644+
Some(1) => {
645+
// Ask for custom branch name
646+
println!();
647+
let new_branch = match input_esc_with_default(
648+
&format!(
649+
"Enter new branch name (base: {})",
650+
selected_branch.yellow()
651+
),
652+
&name,
653+
) {
654+
Some(name) => name.trim().to_string(),
655+
None => return Ok(false),
656+
};
657+
658+
if new_branch.is_empty() {
659+
utils::print_error("Branch name cannot be empty");
660+
return Ok(false);
661+
}
662+
663+
if local_branches.contains(&new_branch) {
664+
utils::print_error(&format!(
665+
"Branch '{}' already exists",
666+
new_branch
667+
));
668+
return Ok(false);
669+
}
670+
671+
(Some(selected_branch.clone()), Some(new_branch))
672+
}
673+
_ => return Ok(false),
674+
}
675+
} else {
676+
(Some(selected_branch.clone()), None)
677+
}
678+
} else {
679+
// Remote branch - check if local branch with same name exists
680+
if local_branches.contains(selected_branch) {
681+
// Local branch with same name exists
682+
println!();
683+
utils::print_warning(&format!(
684+
"A local branch '{}' already exists for remote '{}'",
685+
selected_branch.yellow(),
686+
format!("{}{}", GIT_REMOTE_PREFIX, selected_branch)
687+
.bright_blue()
688+
));
689+
println!();
690+
691+
let use_local_option = if let Some(worktree) =
692+
branch_worktree_map.get(selected_branch)
693+
{
694+
format!(
695+
"Use the existing local branch instead (in use by '{}')",
696+
worktree.bright_red()
697+
)
698+
} else {
699+
"Use the existing local branch instead".to_string()
700+
};
701+
702+
let action_options = vec![
703+
format!(
704+
"Create new branch '{}' from '{}{}'",
705+
name, GIT_REMOTE_PREFIX, selected_branch
706+
),
707+
use_local_option,
708+
"Cancel".to_string(),
709+
];
710+
711+
match Select::with_theme(&get_theme())
712+
.with_prompt("What would you like to do?")
713+
.items(&action_options)
714+
.interact_opt()?
715+
{
716+
Some(0) => {
717+
// Create new branch with worktree name
718+
(
719+
Some(format!(
720+
"{}{}",
721+
GIT_REMOTE_PREFIX, selected_branch
722+
)),
723+
Some(name.clone()),
724+
)
725+
}
726+
Some(1) => {
727+
// Use local branch instead - but check if it's already in use
728+
if let Some(worktree) =
729+
branch_worktree_map.get(selected_branch)
730+
{
731+
println!();
732+
utils::print_error(&format!(
733+
"Branch '{}' is already checked out in worktree '{}'",
734+
selected_branch.yellow(),
735+
worktree.bright_red()
736+
));
737+
println!("Please select a different option.");
738+
return Ok(false);
739+
}
740+
(Some(selected_branch.clone()), None)
741+
}
742+
_ => return Ok(false),
743+
}
744+
} else {
745+
// No conflict, proceed normally
746+
(
747+
Some(format!("{}{}", GIT_REMOTE_PREFIX, selected_branch)),
748+
None,
749+
)
750+
}
751+
}
752+
}
753+
None => return Ok(false),
754+
}
576755
}
577756
}
578-
} else {
579-
None
757+
_ => {
758+
// Create from current HEAD
759+
(None, None)
760+
}
580761
};
581762

582763
// Show preview
583764
println!();
584765
println!("{}", "Preview:".bright_white());
585766
println!(" {} {}", "Name:".bright_black(), final_name.bright_green());
586-
if let Some(branch_name) = &branch {
767+
if let Some(new_branch) = &new_branch_name {
768+
let base_branch_name = branch.as_ref().unwrap();
769+
println!(
770+
" {} {} (from {})",
771+
"New Branch:".bright_black(),
772+
new_branch.yellow(),
773+
base_branch_name.bright_black()
774+
);
775+
} else if let Some(branch_name) = &branch {
587776
println!(" {} {}", "Branch:".bright_black(), branch_name.yellow());
588777
} else {
589778
println!(" {} Current HEAD", "From:".bright_black());
@@ -600,7 +789,15 @@ fn create_worktree_internal(manager: &GitWorktreeManager) -> Result<bool> {
600789
pb.set_message("Creating worktree...");
601790
pb.enable_steady_tick(Duration::from_millis(100));
602791

603-
match manager.create_worktree(&final_name, branch.as_deref()) {
792+
let result = if let Some(new_branch) = &new_branch_name {
793+
// Create worktree with new branch from base branch
794+
manager.create_worktree_with_new_branch(&final_name, new_branch, branch.as_ref().unwrap())
795+
} else {
796+
// Create worktree with existing branch or from HEAD
797+
manager.create_worktree(&final_name, branch.as_deref())
798+
};
799+
800+
match result {
604801
Ok(path) => {
605802
pb.finish_and_clear();
606803
utils::print_success(&format!(

0 commit comments

Comments
 (0)