stax 0.29.4

Fast stacked Git branches and PRs
Documentation
use super::registry::{AgentWorktree, Registry};
use super::util::{ensure_gitignore, open_in_editor, resolve_editor, slugify};
use crate::config::Config;
use crate::engine::BranchMetadata;
use crate::git::GitRepo;
use anyhow::{bail, Context, Result};
use colored::Colorize;
use std::fs;
use std::process::Command;

pub fn run(
    title: String,
    base: Option<String>,
    stack_on: Option<String>,
    open_editor: bool,
    open_cursor: bool,
    open_codex: bool,
    no_hook: bool,
) -> Result<()> {
    let repo = GitRepo::open()?;
    let config = Config::load()?;
    let workdir = repo.workdir()?.to_path_buf();
    let git_dir = repo.git_dir()?.to_path_buf();
    let current = repo.current_branch()?;

    let parent_branch = stack_on.or(base).unwrap_or_else(|| current.clone());

    if repo.branch_commit(&parent_branch).is_err() {
        bail!("Branch '{}' does not exist", parent_branch);
    }

    let slug = slugify(&title);
    if slug.is_empty() {
        bail!(
            "Title '{}' produces an empty slug — use alphanumeric characters",
            title
        );
    }

    let branch_name = config.format_branch_name(&title);

    if repo.branch_commit(&branch_name).is_ok() {
        bail!(
            "Branch '{}' already exists. Use a different title.",
            branch_name
        );
    }

    let worktrees_dir = workdir.join(&config.agent.worktrees_dir);
    let worktree_path = worktrees_dir.join(&slug);

    if worktree_path.exists() {
        bail!(
            "Worktree path '{}' already exists.",
            worktree_path.display()
        );
    }

    fs::create_dir_all(&worktrees_dir).with_context(|| {
        format!(
            "Failed to create worktrees directory: {}",
            worktrees_dir.display()
        )
    })?;

    ensure_gitignore(&workdir, &config.agent.worktrees_dir)?;

    let parent_rev = repo.branch_commit(&parent_branch)?;

    let output = Command::new("git")
        .args([
            "worktree",
            "add",
            "-b",
            &branch_name,
            worktree_path.to_str().context("Non-UTF-8 worktree path")?,
            &parent_branch,
        ])
        .current_dir(&workdir)
        .output()
        .context("Failed to run git worktree add")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
        bail!("git worktree add failed: {}", stderr);
    }

    let meta = BranchMetadata::new(&parent_branch, &parent_rev);
    meta.write(repo.inner(), &branch_name)?;

    let mut registry = Registry::load(&git_dir)?;
    registry.add(AgentWorktree {
        name: slug.clone(),
        branch: branch_name.clone(),
        path: worktree_path.clone(),
        created_at: chrono::Local::now().to_rfc3339(),
    });
    registry.save()?;

    println!(
        "{}  worktree '{}' → branch '{}'",
        "Created".green().bold(),
        slug.cyan(),
        branch_name.blue()
    );
    println!("  Path:   {}", worktree_path.display().to_string().dimmed());
    println!("  Parent: {}", parent_branch.dimmed());

    if !no_hook {
        if let Some(hook) = &config.agent.post_create_hook {
            if !hook.is_empty() {
                println!("\nRunning post-create hook...");
                let status = Command::new("sh")
                    .args(["-c", hook])
                    .current_dir(&worktree_path)
                    .status()
                    .context("Failed to run post_create_hook")?;
                if !status.success() {
                    eprintln!("{}", "  Post-create hook failed (continuing)".yellow());
                }
            }
        }
    }

    let editor_cmd = resolve_editor(&config, open_cursor, open_codex, open_editor);
    if let Some(cmd) = editor_cmd {
        open_in_editor(&cmd, &worktree_path)?;
    } else {
        println!("\n  Tip: {}", format!("stax agent open {}", slug).cyan());
    }

    Ok(())
}