workmux 0.1.212

An opinionated workflow tool that orchestrates git worktrees and tmux
use anyhow::{Result, anyhow};
use std::path::PathBuf;

use crate::git;
use crate::sandbox;
use tracing::{debug, info};

use super::cleanup::{self, get_worktree_mode};
use super::context::WorkflowContext;
use super::types::RemoveResult;

pub fn fallback_worktree_path(handle: &str, context: &WorkflowContext) -> Result<Option<PathBuf>> {
    let base_dir = if let Some(ref worktree_dir) = context.config.worktree_dir {
        crate::util::expand_worktree_dir(worktree_dir, &context.main_worktree_root)?
    } else {
        let project_name = context
            .main_worktree_root
            .file_name()
            .and_then(|n| n.to_str())
            .ok_or_else(|| anyhow!("Could not determine project name"))?;
        context
            .main_worktree_root
            .parent()
            .ok_or_else(|| anyhow!("Could not determine parent directory"))?
            .join(format!("{}__worktrees", project_name))
    };

    let path = base_dir.join(handle);
    let Some(admin_dir) = git::linked_worktree_admin_dir(&path) else {
        return Ok(None);
    };
    let expected_parent = context.git_common_dir.join("worktrees");
    Ok((!admin_dir.is_dir() && admin_dir.starts_with(expected_parent)).then_some(path))
}

/// Remove a worktree without merging
pub fn remove(
    handle: &str,
    force: bool,
    keep_branch: bool,
    context: &WorkflowContext,
) -> Result<RemoveResult> {
    info!(handle = handle, force, keep_branch, "remove:start");

    // Get worktree path and branch - this also validates that the worktree exists
    // Smart resolution: try handle first, then branch name
    let (worktree_path, branch_name) = match git::find_worktree(handle) {
        Ok(worktree) => worktree,
        Err(e) => {
            if let Some(path) = fallback_worktree_path(handle, context)? {
                (path, String::new())
            } else {
                return Err(anyhow!(
                    "Worktree '{}' not found. Use 'workmux list' to see available worktrees.",
                    handle
                )
                .context(e));
            }
        }
    };

    // Extract actual handle from worktree path (directory name)
    // User may have provided branch name (with slashes) but window names use handle (with dashes)
    let actual_handle = worktree_path
        .file_name()
        .and_then(|n| n.to_str())
        .ok_or_else(|| {
            anyhow!(
                "Could not derive handle from worktree path: {}",
                worktree_path.display()
            )
        })?;

    debug!(handle = actual_handle, branch = branch_name, path = %worktree_path.display(), "remove:worktree resolved");

    // Capture mode BEFORE cleanup (cleanup removes the metadata)
    let mode = get_worktree_mode(actual_handle);

    // Safety Check: Prevent deleting the main worktree itself, regardless of branch.
    let is_main_worktree = match (
        worktree_path.canonicalize(),
        context.main_worktree_root.canonicalize(),
    ) {
        (Ok(canon_wt_path), Ok(canon_main_path)) => {
            // Best case: both paths exist and can be resolved. This is the most reliable check.
            canon_wt_path == canon_main_path
        }
        _ => {
            // Fallback: If canonicalization fails on either path (e.g., directory was
            // manually removed, broken symlink), compare the raw paths provided by git.
            // This is a critical safety net.
            worktree_path == context.main_worktree_root
        }
    };

    if is_main_worktree {
        return Err(anyhow!(
            "Cannot remove branch '{}' because it is checked out in the main worktree at '{}'. \
            Switch the main worktree to a different branch first, or create a linked worktree for '{}'.",
            branch_name,
            context.main_worktree_root.display(),
            branch_name
        ));
    }

    if branch_name.is_empty() && !keep_branch {
        return Err(anyhow!(
            "Worktree '{}' has broken Git metadata, so its branch cannot be determined. \
            Use --keep-branch to remove only the worktree directory.",
            actual_handle
        ));
    }

    // Safety Check: Prevent deleting the main branch by name (secondary check)
    if branch_name == context.main_branch {
        return Err(anyhow!(
            "Cannot delete the main branch ('{}')",
            context.main_branch
        ));
    }

    if worktree_path.exists()
        && !git::has_missing_admin_dir(&worktree_path)
        && git::has_uncommitted_changes(&worktree_path)?
        && !force
    {
        return Err(anyhow!(
            "Worktree has uncommitted changes. Use --force to delete anyway."
        ));
    }

    // Note: Unmerged branch check removed - git branch -d/D handles this natively
    // The CLI provides a user-friendly confirmation prompt before calling this function

    // Stop any running containers for this worktree before killing the window.
    // This is necessary because tmux kill-window sends SIGHUP which doesn't allow
    // the supervisor's Drop handler to run. We try unconditionally since sandbox
    // may have been enabled via --sandbox flag even if disabled in config.
    sandbox::stop_containers_for_handle(actual_handle);

    info!(branch = %branch_name, keep_branch, "remove:cleanup start");
    let cleanup_result = cleanup::cleanup(
        context,
        &branch_name,
        actual_handle,
        &worktree_path,
        force,
        keep_branch,
        false, // no_hooks: run hooks normally for user-initiated remove
    )?;

    // Navigate to the main branch window/session and close the source
    cleanup::navigate_to_target_and_close(
        context.mux.as_ref(),
        &context.prefix,
        &context.main_branch,
        actual_handle,
        &cleanup_result,
        mode,
    )?;

    Ok(RemoveResult {
        branch_removed: branch_name.to_string(),
    })
}