use colored::Colorize;
use std::io::{self, Write};
use crate::{
constants,
core::project::{clean_branch_name, find_git_directory, find_project_root_from, is_orphaned_worktree, find_valid_git_directory, find_project_root},
error::{Error, Result},
git, hooks,
};
pub fn run(branch_name: Option<&str>, force: bool) -> Result<()> {
if let Some(branch) = branch_name {
if let Ok(project_root) = find_project_root() {
let potential_worktree_path = project_root.join(branch);
if is_orphaned_worktree(&potential_worktree_path) {
println!("{}", "⚠️ Detected orphaned worktree (stale git reference)".yellow());
return remove_orphaned_worktree(&potential_worktree_path, branch, force);
}
}
}
let git_dir = find_git_directory()?;
let worktrees = git::list_worktrees(Some(&git_dir))?;
if worktrees.is_empty() {
println!("{}", "No worktrees found.".yellow());
return Ok(());
}
let target_worktree = find_target_worktree(&worktrees, branch_name)?;
if target_worktree.bare {
return Err(Error::msg("Cannot remove the main (bare) repository."));
}
if is_orphaned_worktree(&target_worktree.path) {
let branch_display = get_branch_display(target_worktree);
println!("{}", "⚠️ Detected orphaned worktree (stale git reference)".yellow());
return remove_orphaned_worktree(&target_worktree.path, branch_display, force);
}
let branch_display = get_branch_display(target_worktree);
println!("{}", "About to remove worktree:".cyan().bold());
println!(" {}: {}", "Path".dimmed(), target_worktree.path.display());
println!(" {}: {}", "Branch".dimmed(), branch_display.green());
let current_dir = std::env::current_dir()?;
let will_remove_current = current_dir.starts_with(&target_worktree.path);
if will_remove_current {
println!(
"\n{}",
"⚠️ You are currently in this worktree. You will be moved to the project root after removal.".yellow()
);
}
if !force {
print!("\n{}", "Are you sure you want to remove this worktree? (y/N): ".cyan());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let confirmation = input.trim().to_lowercase();
if confirmation != "y" && confirmation != "yes" {
println!("{}", "Removal cancelled.".yellow());
return Ok(());
}
}
let project_root = if let Some(parent) = target_worktree.path.parent() {
find_project_root_from(parent)?
} else {
find_project_root_from(&target_worktree.path)?
};
hooks::execute_hooks(
"preRemove",
&target_worktree.path,
&[
("branchName", branch_display),
("worktreePath", target_worktree.path.to_str().unwrap()),
],
)?;
let main_branches = constants::PROTECTED_BRANCHES;
let git_working_dir = worktrees
.iter()
.find(|wt| {
wt.path != target_worktree.path
&& wt
.branch
.as_ref()
.map(|b| {
let clean_branch = b.strip_prefix("refs/heads/").unwrap_or(b);
main_branches.contains(&clean_branch)
})
.unwrap_or(false)
})
.or_else(|| {
worktrees.iter().find(|wt| wt.path != target_worktree.path)
})
.ok_or_else(|| Error::msg("No other worktrees found to execute git command from."))?;
println!("\n{}", "Removing worktree...".cyan());
git::execute_streaming(
&["worktree", "remove", target_worktree.path.to_str().unwrap(), "--force"],
Some(&git_working_dir.path),
)?;
println!(
"{}",
format!("✓ Worktree removed: {}", target_worktree.path.display()).green()
);
if !main_branches.contains(&branch_display) {
match git::execute_capture(&["branch", "-d", branch_display], Some(&git_working_dir.path)) {
Ok(_) => {
println!("{}", format!("✓ Branch deleted: {}", branch_display).green());
}
Err(e) => {
if e.to_string().contains("not fully merged") {
println!(
"{}",
format!("⚠️ Branch '{}' has unmerged changes", branch_display).yellow()
);
let should_force_delete = if force {
true
} else {
print!("{}", "Force delete the branch? (y/N): ".cyan());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let force_delete = input.trim().to_lowercase();
force_delete == "y" || force_delete == "yes"
};
if should_force_delete {
match git::execute_streaming(&["branch", "-D", branch_display], Some(&git_working_dir.path)) {
Ok(_) => {
println!("{}", format!("✓ Branch force deleted: {}", branch_display).green());
}
Err(e) => {
println!(
"{}",
format!("❌ Failed to delete branch '{}': {}", branch_display, e).red()
);
}
}
} else {
println!(
"{}",
format!("⚠️ Branch '{}' was not deleted", branch_display).yellow()
);
}
} else {
println!(
"{}",
format!("❌ Failed to delete branch '{}': {}", branch_display, e).red()
);
}
}
}
} else {
println!(
"{}",
format!("✓ Branch: {} (preserved - main branch)", branch_display).green()
);
}
if will_remove_current {
std::env::set_current_dir(&project_root)?;
}
hooks::execute_hooks(
"postRemove",
&project_root,
&[
("branchName", branch_display),
("worktreePath", target_worktree.path.to_str().unwrap()),
],
)?;
if will_remove_current {
println!(
"{}",
format!("✓ Please navigate to project root: {}", project_root.display()).green()
);
}
Ok(())
}
fn find_target_worktree<'a>(worktrees: &'a [git::Worktree], branch_name: Option<&str>) -> Result<&'a git::Worktree> {
match branch_name {
None => find_current_worktree(worktrees),
Some(target_branch) => find_worktree_by_branch(worktrees, target_branch),
}
}
fn find_current_worktree(worktrees: &[git::Worktree]) -> Result<&git::Worktree> {
let current_dir = std::env::current_dir()?;
worktrees
.iter()
.find(|wt| current_dir.starts_with(&wt.path))
.ok_or_else(|| Error::msg("Not in a git worktree. Please specify a branch to remove."))
}
fn find_worktree_by_branch<'a>(worktrees: &'a [git::Worktree], target_branch: &str) -> Result<&'a git::Worktree> {
if let Some(worktree) = find_by_branch_name(worktrees, target_branch) {
return Ok(worktree);
}
if let Some(worktree) = find_by_path_name(worktrees, target_branch) {
return Ok(worktree);
}
show_available_worktrees(worktrees);
Err(Error::msg(format!("Worktree for '{}' not found", target_branch)))
}
fn find_by_branch_name<'a>(worktrees: &'a [git::Worktree], target_branch: &str) -> Option<&'a git::Worktree> {
worktrees.iter().find(|wt| {
wt.branch
.as_ref()
.map(|b| clean_branch_name(b) == target_branch)
.unwrap_or(false)
})
}
fn find_by_path_name<'a>(worktrees: &'a [git::Worktree], target_branch: &str) -> Option<&'a git::Worktree> {
worktrees.iter().find(|wt| {
wt.path
.file_name()
.and_then(|name| name.to_str())
.map(|name| name == target_branch)
.unwrap_or(false)
})
}
fn show_available_worktrees(worktrees: &[git::Worktree]) {
println!("{}", "Error: Worktree not found.".red());
println!("\n{}", "Available worktrees:".yellow());
for worktree in worktrees {
let branch_display = get_branch_display(worktree);
println!(
" {} -> {}",
branch_display.green(),
worktree.path.display().to_string().dimmed()
);
}
}
fn get_branch_display(worktree: &git::Worktree) -> &str {
worktree
.branch
.as_ref()
.map(|b| clean_branch_name(b))
.unwrap_or_else(|| {
if worktree.bare {
"(bare)"
} else {
&worktree.head[..8.min(worktree.head.len())]
}
})
}
fn remove_orphaned_worktree(worktree_path: &std::path::Path, branch_name: &str, force: bool) -> Result<()> {
use std::fs;
println!("{}", "About to remove orphaned worktree:".cyan().bold());
println!(" {}: {}", "Path".dimmed(), worktree_path.display());
println!(" {}: {}", "Name".dimmed(), branch_name.green());
println!(" {}: {}", "Status".dimmed(), "Orphaned (stale reference)".yellow());
let current_dir = std::env::current_dir()?;
let will_remove_current = current_dir.starts_with(worktree_path);
if will_remove_current {
println!(
"\n{}",
"⚠️ You are currently in this worktree. You will be moved to the project root after removal.".yellow()
);
}
if !force {
print!("\n{}", "Are you sure you want to remove this orphaned worktree? (y/N): ".cyan());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let confirmation = input.trim().to_lowercase();
if confirmation != "y" && confirmation != "yes" {
println!("{}", "Removal cancelled.".yellow());
return Ok(());
}
}
let project_root = find_project_root()?;
if will_remove_current {
std::env::set_current_dir(&project_root)?;
}
println!("\n{}", "Removing orphaned worktree directory...".cyan());
fs::remove_dir_all(worktree_path).map_err(|e| {
Error::msg(format!(
"Failed to remove directory {}: {}",
worktree_path.display(),
e
))
})?;
println!(
"{}",
format!("✓ Directory removed: {}", worktree_path.display()).green()
);
if let Ok(valid_git_dir) = find_valid_git_directory(&project_root) {
println!("{}", "Pruning stale worktree references...".cyan());
match git::prune_worktrees(&valid_git_dir) {
Ok(_) => {
println!("{}", "✓ Worktree references pruned".green());
}
Err(e) => {
println!(
"{}",
format!("⚠️ Failed to prune worktree references: {}", e).yellow()
);
}
}
}
if will_remove_current {
println!(
"{}",
format!("✓ Moved to project root: {}", project_root.display()).green()
);
}
println!("\n{}", "Note: Orphaned worktree removed. Hooks were skipped due to invalid git state.".dimmed());
Ok(())
}