worktrunk 0.37.1

A CLI for Git worktree management, designed for parallel AI agent workflows
Documentation
use anyhow::Context;
use std::path::PathBuf;
use worktrunk::config::UserConfig;
use worktrunk::git::Repository;

use super::command_executor::CommandContext;

/// Shared execution context for command handlers that operate on the current worktree.
///
/// Centralizes the common "repo + branch + config + cwd" setup so individual handlers
/// can focus on their core logic while sharing consistent error messaging.
///
/// This helper is used for commands that explicitly act on "where the user is standing"
/// (e.g., `beta` and `merge`) and therefore need all of these pieces together. Commands that
/// inspect multiple worktrees or run without a config/branch requirement (`list`, the picker,
/// some `worktree` helpers) still call `Repository::current()` directly so they can operate in
/// broader contexts without forcing config loads or branch resolution.
pub struct CommandEnv {
    pub repo: Repository,
    /// Current branch name, if on a branch (None in detached HEAD state).
    pub branch: Option<String>,
    pub config: UserConfig,
    /// Canonical absolute path to the worktree root (via `git rev-parse --show-toplevel`).
    pub worktree_path: PathBuf,
}

impl CommandEnv {
    /// Load the command environment for the current worktree.
    ///
    /// Resolves the worktree path from the current directory. The branch is
    /// populated when available but not required — commands that need a branch
    /// should call `require_branch()` after construction.
    pub fn for_action(config: UserConfig) -> anyhow::Result<Self> {
        let repo = Repository::current()?;
        let current_wt = repo.current_worktree();
        let worktree_path = current_wt.root()?;
        let branch = current_wt
            .branch()
            .context("Failed to determine current branch")?;

        Ok(Self {
            repo,
            branch,
            config,
            worktree_path,
        })
    }

    /// Load the command environment for a named worktree (by branch name).
    ///
    /// Resolves the worktree path from the branch name rather than using
    /// the current working directory.
    pub fn for_branch(config: UserConfig, branch: &str) -> anyhow::Result<Self> {
        let repo = Repository::current()?;
        let worktree_path = repo
            .worktree_for_branch(branch)?
            .ok_or_else(|| anyhow::anyhow!("no worktree for branch '{branch}'"))?;

        Ok(Self {
            repo,
            branch: Some(branch.to_string()),
            config,
            worktree_path,
        })
    }

    /// Load the command environment without requiring a branch.
    ///
    /// Use this for commands that can operate in detached HEAD state,
    /// such as running hooks (where `{{ branch }}` expands to "HEAD" if detached).
    pub fn for_action_branchless() -> anyhow::Result<Self> {
        let repo = Repository::current()?;
        let current_wt = repo.current_worktree();
        let worktree_path = current_wt.root()?;
        // Propagate git errors (broken repo, missing git) but allow None for detached HEAD
        let branch = current_wt
            .branch()
            .context("Failed to determine current branch")?;
        let config = UserConfig::load().context("Failed to load config")?;

        Ok(Self {
            repo,
            branch,
            config,
            worktree_path,
        })
    }

    /// Build a `CommandContext` tied to this environment.
    pub fn context(&self, yes: bool) -> CommandContext<'_> {
        CommandContext::new(
            &self.repo,
            &self.config,
            self.branch.as_deref(),
            &self.worktree_path,
            yes,
        )
    }

    /// Get branch name, returning error if in detached HEAD state.
    pub fn require_branch(&self, action: &str) -> anyhow::Result<&str> {
        self.branch.as_deref().ok_or_else(|| {
            worktrunk::git::GitError::DetachedHead {
                action: Some(action.into()),
            }
            .into()
        })
    }

    /// Get the project identifier for per-project config lookup.
    ///
    /// Uses the remote URL if available, otherwise the canonical repository path.
    /// Returns None only if the path is not valid UTF-8.
    pub fn project_id(&self) -> Option<String> {
        self.repo.project_identifier().ok()
    }

    /// Get all resolved config with defaults applied.
    pub fn resolved(&self) -> worktrunk::config::ResolvedConfig {
        self.config.resolved(self.project_id().as_deref())
    }
}