worktrunk 0.35.3

A CLI for Git worktree management, designed for parallel AI agent workflows
Documentation
//! Worktree operations with file-based shell integration.
//!
//! # The Directory Change Problem
//!
//! Worktree commands (`switch`, `remove`, `merge`) need to change the user's working directory,
//! but there's a fundamental Unix limitation: **child processes cannot change their parent's
//! working directory**. This is a security feature and core Unix design principle.
//!
//! When a user runs `wt switch my-feature`, the `wt` binary runs as a child process of the shell.
//! The binary can change *its own* working directory, but when it exits, the parent shell remains
//! in the original directory.
//!
//! # Solution: File-Based Directive Passing
//!
//! Shell wrappers create a temp file and set `WORKTRUNK_DIRECTIVE_FILE` to its path:
//!
//! 1. Shell wrapper creates temp file via `mktemp`
//! 2. Shell wrapper sets `WORKTRUNK_DIRECTIVE_FILE=/path/to/temp`
//! 3. wt binary writes commands like `cd '/path'` to that file
//! 4. Shell wrapper sources the file after wt exits
//!
//! ## Without Shell Integration (Direct Binary Call)
//!
//! ```bash
//! $ wt switch my-feature
//! Created new branch and worktree for 'my-feature' @ /path/to/worktree
//!
//! Run `wt config shell install` to enable automatic cd
//!
//! $ pwd
//! /original/directory  # ← User is still here!
//! ```
//!
//! The binary performs git operations and prints user-friendly messages, but **cannot** change
//! the parent shell's directory. User must manually `cd` to the worktree.
//!
//! ## With Shell Integration
//!
//! ```bash
//! $ wt switch my-feature
//! Created new branch and worktree for 'my-feature' @ /path/to/worktree
//!
//! $ pwd
//! /path/to/worktree  # ← Automatically changed!
//! ```
//!
//! When shell integration is enabled (`eval "$(wt config shell init bash)"`), the shell wrapper:
//!
//! 1. Creates a temp file and sets `WORKTRUNK_DIRECTIVE_FILE` to its path
//! 2. Runs the wt binary (which writes `cd '/path'` to the temp file)
//! 3. Sources the temp file after wt exits
//!
//! # Implementation Details
//!
//! Result types (`SwitchResult`, `RemoveResult`) are pure data structures that only contain
//! operation results. All presentation logic is handled by the `output` module:
//!
//! - `output::handle_switch_output()`: Formats and outputs switch operation results
//! - `output::handle_remove_output()`: Formats and outputs remove operation results
//!
//! The output handlers check `is_shell_integration_active()` to determine if hints should
//! be suppressed (when shell integration is already configured).
//!
//! ## Exit Code Semantics
//!
//! When using `-x` to execute commands after switching:
//!
//! - **If wt operation fails**: Returns wt's exit code (command never executes)
//! - **If wt succeeds but command fails**: Returns the command's exit code
//! - **Rationale**: Enables command chaining with proper error propagation
//!
//! Example:
//! ```bash
//! wt switch feature -x "cargo build" && cargo test
//! # If wt fails (e.g., worktree doesn't exist), cargo build never runs
//! # If cargo build fails, cargo test doesn't run (exit code propagates)
//! ```
//!
//! The shell wrapper is generated by `wt config shell init <shell>` from templates in `templates/`.

pub(crate) mod hooks;
mod push;
mod resolve;
mod switch;
mod types;

use std::path::{Path, PathBuf};

use super::branch_deletion::{BranchDeletionResult, delete_branch_if_safe};
use super::process::generate_removing_path;
use worktrunk::git::Repository;

// Re-export public types and functions
pub use push::{handle_no_ff_merge, handle_push};
pub(crate) use resolve::paths_match;
pub use resolve::{
    compute_worktree_path, is_worktree_at_expected_path, offer_bare_repo_worktree_path_fix,
    path_mismatch, resolve_worktree_arg, worktree_display_name,
};
pub use switch::{execute_switch, plan_switch};
pub use types::{
    BranchDeletionMode, MergeOperations, OperationMode, RemoveResult, SwitchBranchInfo, SwitchPlan,
    SwitchResult,
};

/// Execute core worktree removal: stop fsmonitor, remove worktree, delete branch.
///
/// Uses the fast path (rename into `.git/wt/trash/` + prune) when possible,
/// falling back to `git worktree remove` on cross-filesystem setups.
/// On the fast path the directory disappears instantly; the caller is responsible
/// for cleaning up the staged trash entry (returned via `staged_path`).
///
/// `branch_result` is the raw `delete_branch_if_safe` outcome, preserved so
/// callers can handle branch deletion failures independently:
/// - The picker ignores it (best-effort in TUI context)
/// - The output handler processes it for user-facing display
///
/// `branch_result` is `None` when deletion was not attempted (no branch name,
/// or `deletion_mode.should_keep()`).
pub struct RemovalOutput {
    pub branch_result: Option<anyhow::Result<BranchDeletionResult>>,
    /// Path to the staged trash directory (fast path only). Caller should
    /// `remove_dir_all` this — either synchronously or in the background.
    pub staged_path: Option<PathBuf>,
}

pub fn execute_removal(
    repo: &Repository,
    worktree_path: &Path,
    branch_name: Option<&str>,
    deletion_mode: BranchDeletionMode,
    target_branch: Option<&str>,
    force_worktree: bool,
) -> anyhow::Result<RemovalOutput> {
    // Stop fsmonitor daemon (best effort — prevents zombie daemons when using
    // builtin fsmonitor). Must happen while the worktree path still exists.
    let _ = repo
        .worktree_at(worktree_path)
        .run_command(&["fsmonitor--daemon", "stop"]);

    // Fast path: rename into .git/wt/trash/ (instant on same filesystem),
    // then prune git metadata. Falls back to `git worktree remove` if the
    // rename fails (cross-filesystem, permissions, Windows file locking).
    let staged_path = stage_worktree_removal(repo, worktree_path);
    if staged_path.is_none() {
        repo.remove_worktree(worktree_path, force_worktree)?;
    }

    // Delete branch if safe
    let branch_result = if let Some(branch) = branch_name
        && !deletion_mode.should_keep()
    {
        let target = target_branch.unwrap_or("HEAD");
        Some(delete_branch_if_safe(
            repo,
            branch,
            target,
            deletion_mode.is_force(),
        ))
    } else {
        None
    };

    Ok(RemovalOutput {
        branch_result,
        staged_path,
    })
}

/// Rename a worktree into `.git/wt/trash/` and prune git metadata.
///
/// Returns `Some(staged_path)` on success, `None` if the rename failed.
/// Used internally by `execute_removal` and by the output handler's
/// `execute_instant_removal_or_fallback` (which builds shell command strings
/// and adds a placeholder directory — needs the raw staged path).
pub(crate) fn stage_worktree_removal(repo: &Repository, worktree_path: &Path) -> Option<PathBuf> {
    let trash_dir = repo.wt_trash_dir();
    let _ = std::fs::create_dir_all(&trash_dir);
    let staged_path = generate_removing_path(&trash_dir, worktree_path);

    if std::fs::rename(worktree_path, &staged_path).is_ok() {
        if let Err(e) = repo.prune_worktrees() {
            log::debug!("Failed to prune worktrees after rename: {e}");
        }
        Some(staged_path)
    } else {
        None
    }
}