stax 0.86.3

Fast stacked Git branches and PRs
Documentation
use super::shared::{
    build_launch_spec, create_worktree_for_resolved_branch, default_create_base,
    derive_unique_worktree_name, emit_shell_payload, ensure_gitignore,
    ensure_managed_worktrees_root, find_worktree, format_create_message, format_go_message,
    generate_random_lane_slug, managed_worktrees_dir, pick_branch_interactively,
    resolve_branch_name, run_blocking_hook, spawn_background_hook, LaunchOptions,
};
use crate::commands::shell_setup;
use crate::config::Config;
use crate::git::GitRepo;
use anyhow::{bail, Context, Result};
use colored::Colorize;
use std::fs;

#[allow(clippy::too_many_arguments)]
pub fn run(
    name: Option<String>,
    from: Option<String>,
    pick: bool,
    worktree_name: Option<String>,
    no_verify: bool,
    shell_output: bool,
    agent: Option<String>,
    model: Option<String>,
    run: Option<String>,
    tmux: bool,
    tmux_session: Option<String>,
    args: Vec<String>,
    yolo: bool,
    agent_args: Vec<String>,
) -> Result<()> {
    if pick && name.is_some() {
        bail!("Use either a name or --pick, not both.");
    }

    let repo = GitRepo::open()?;
    let config = Config::load()?;
    let launch_options = LaunchOptions {
        agent,
        model,
        run,
        tmux,
        tmux_session,
        args,
        yolo,
        agent_args,
    };

    if let Some(ref target) = name {
        if let Some(worktree) = find_worktree(&repo, target)? {
            let launch = build_launch_spec(&config, &launch_options, &worktree.name)?;
            format_go_message(&worktree);
            if !no_verify {
                spawn_background_hook(
                    config.worktree.hooks.post_go.as_deref(),
                    &worktree.path,
                    "post_go",
                )?;
            }

            if shell_output {
                emit_shell_payload(&worktree.path, launch.as_ref());
            } else if let Some(launch) = launch.as_ref() {
                launch.execute_in(&worktree.path)?;
            } else {
                println!();
                println!("{}", "Current shell did not move automatically.".yellow());
                println!("  {}", format!("cd {}", worktree.path.display()).cyan());
                if !shell_setup::is_installed() {
                    println!();
                    println!(
                        "{}",
                        "Tip: add shell integration for automatic cd:".dimmed()
                    );
                    println!("  {}", "stax setup".cyan());
                }
            }

            return Ok(());
        }
    }

    let input_name = match (pick, name) {
        (true, _) => pick_branch_interactively(&repo)?,
        (false, Some(name)) => name,
        (false, None) => generate_random_lane_slug(&repo, &config)?,
    };

    let resolved_branch = resolve_branch_name(&repo, &config, &input_name)?;
    let branch_name = resolved_branch.name.clone();
    if let Some(worktree) = find_worktree(&repo, &branch_name)? {
        let launch = build_launch_spec(&config, &launch_options, &worktree.name)?;
        format_go_message(&worktree);
        if !no_verify {
            spawn_background_hook(
                config.worktree.hooks.post_go.as_deref(),
                &worktree.path,
                "post_go",
            )?;
        }

        if shell_output {
            emit_shell_payload(&worktree.path, launch.as_ref());
        } else if let Some(launch) = launch.as_ref() {
            launch.execute_in(&worktree.path)?;
        } else {
            println!();
            println!("{}", "Current shell did not move automatically.".yellow());
            println!("  {}", format!("cd {}", worktree.path.display()).cyan());
            if !shell_setup::is_installed() {
                println!();
                println!(
                    "{}",
                    "Tip: add shell integration for automatic cd:".dimmed()
                );
                println!("  {}", "stax setup".cyan());
            }
        }
        return Ok(());
    }

    let base_branch = if resolved_branch.needs_base_branch() {
        let base_branch = from.unwrap_or(default_create_base(&repo)?);
        repo.branch_commit(&base_branch)
            .with_context(|| format!("Base branch '{}' does not exist", base_branch))?;
        Some(base_branch)
    } else {
        None
    };

    let worktree_name = worktree_name.unwrap_or(derive_unique_worktree_name(&repo, &branch_name)?);
    let launch = build_launch_spec(&config, &launch_options, &worktree_name)?;
    let worktrees_dir = managed_worktrees_dir(&repo, &config)?;
    let worktree_path = worktrees_dir.join(&worktree_name);
    if worktree_path.exists() {
        bail!(
            "Worktree path '{}' already exists.",
            worktree_path.display()
        );
    }

    fs::create_dir_all(&worktrees_dir)?;
    ensure_managed_worktrees_root(&repo, &config, &worktrees_dir)?;
    let main_repo_workdir = repo.main_repo_workdir()?;
    ensure_gitignore(&main_repo_workdir, &config.worktree.root_dir)?;
    create_worktree_for_resolved_branch(
        &repo,
        &resolved_branch,
        &worktree_path,
        base_branch.as_deref(),
    )?;

    let copied_files = repo.tracked_file_count_at(&worktree_path).unwrap_or(0);
    let repo_name = main_repo_workdir
        .file_name()
        .map(|name| name.to_string_lossy().into_owned())
        .unwrap_or_else(|| "repo".to_string());
    let from_label = resolved_branch.source_label(&repo, base_branch.as_deref());
    format_create_message(
        &repo_name,
        &worktree_name,
        &branch_name,
        &from_label,
        copied_files,
        resolved_branch.is_existing(),
    );

    if !no_verify {
        run_blocking_hook(
            config.worktree.hooks.post_create.as_deref(),
            &worktree_path,
            "post_create",
        )?;
        spawn_background_hook(
            config.worktree.hooks.post_start.as_deref(),
            &worktree_path,
            "post_start",
        )?;
    }

    if shell_output {
        emit_shell_payload(&worktree_path, launch.as_ref());
    } else if let Some(launch) = launch.as_ref() {
        launch.execute_in(&worktree_path)?;
    } else {
        println!();
        println!("{}", "Current shell did not move automatically.".yellow());
        println!("  {}", format!("cd {}", worktree_path.display()).cyan());
        if !shell_setup::is_installed() {
            println!();
            println!(
                "{}",
                "Tip: add shell integration for automatic cd:".dimmed()
            );
            println!("  {}", "stax setup".cyan());
        }
    }

    Ok(())
}