git-workty 0.3.3

Git worktrees as daily-driver workspaces
Documentation
use crate::git::GitRepo;
use crate::status::is_worktree_dirty;
use crate::ui::{print_success, print_warning};
use crate::worktree::{find_worktree, list_worktrees};
use anyhow::{bail, Context, Result};
use dialoguer::Confirm;
use is_terminal::IsTerminal;
use std::process::Command;

pub struct RmOptions {
    pub name: String,
    pub force: bool,
    pub delete_branch: bool,
    pub yes: bool,
}

pub fn execute(repo: &GitRepo, opts: RmOptions) -> Result<()> {
    let worktrees = list_worktrees(repo)?;

    let wt = find_worktree(&worktrees, &opts.name).ok_or_else(|| {
        anyhow::anyhow!(
            "Worktree '{}' not found. Use `git workty list` to see available worktrees.",
            opts.name
        )
    })?;

    let current_path = std::env::current_dir().context("Failed to get current directory")?;
    if wt.path == current_path {
        bail!("Cannot remove the current worktree. Change to a different worktree first.");
    }

    if wt.is_main_worktree(repo) {
        bail!("Cannot remove the main worktree (original repository clone)");
    }

    let is_dirty = is_worktree_dirty(wt);
    if is_dirty && !opts.force {
        bail!(
            "Worktree '{}' has uncommitted changes. Use --force to remove anyway.",
            opts.name
        );
    }

    if is_dirty {
        print_warning(&format!(
            "Worktree '{}' has uncommitted changes (--force specified)",
            opts.name
        ));
    }

    if !opts.yes && std::io::stdin().is_terminal() {
        let confirm = Confirm::new()
            .with_prompt(format!(
                "Remove worktree '{}'{}?",
                opts.name,
                if opts.delete_branch {
                    " and its branch"
                } else {
                    ""
                }
            ))
            .default(false)
            .interact()?;

        if !confirm {
            bail!("Aborted");
        }
    }

    let branch_name = wt.branch_short.clone();
    let wt_path = wt.path.clone();
    let path_str = wt_path
        .to_str()
        .ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8: {:?}", wt_path))?;

    let mut args = vec!["worktree", "remove"];
    if opts.force {
        args.push("--force");
    }
    args.push(path_str);

    let output = Command::new("git")
        .current_dir(&repo.root)
        .args(&args)
        .output()
        .context("Failed to remove worktree")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("Failed to remove worktree: {}", stderr.trim());
    }

    print_success(&format!("Removed worktree '{}'", opts.name));

    if opts.delete_branch {
        if let Some(branch) = branch_name {
            let output = Command::new("git")
                .current_dir(&repo.root)
                .args(["branch", "-d", &branch])
                .output();

            match output {
                Ok(o) if o.status.success() => {
                    print_success(&format!("Deleted branch '{}'", branch));
                }
                Ok(o) => {
                    let stderr = String::from_utf8_lossy(&o.stderr);
                    print_warning(&format!(
                        "Could not delete branch '{}': {}",
                        branch,
                        stderr.trim()
                    ));
                    eprintln!("Hint: Use `git branch -D {}` to force delete.", branch);
                }
                Err(e) => {
                    print_warning(&format!("Could not delete branch '{}': {}", branch, e));
                }
            }
        }
    }

    Ok(())
}