thor-wt 0.2.0

Worktree workflow commands for Thor
Documentation
use thor_core::{find_repo, list_worktrees, remove_worktree};
use std::path::PathBuf;
use std::process::Command;

/// Options for the done command.
pub struct DoneOpts {
    pub no_push: bool,
    pub pr: bool,
}

/// Result of a done operation.
pub enum DoneResult {
    Merged { target_path: PathBuf },
    PrCreated { url: String, target_path: PathBuf },
}

/// Finish the current branch: merge into target or create a PR, then clean up.
pub async fn done(target: &str, opts: &DoneOpts) -> anyhow::Result<DoneResult> {
    let repo = find_repo()?;
    let worktrees = list_worktrees(&repo).await?;
    let cwd = std::env::current_dir()?;

    // Find current branch
    let current_wt = worktrees.iter()
        .find(|wt| cwd.starts_with(&wt.path))
        .ok_or_else(|| anyhow::anyhow!("Not in a worktree"))?;

    let current_branch = current_wt.branch.as_ref()
        .ok_or_else(|| anyhow::anyhow!("Current worktree has no branch (detached HEAD)"))?
        .clone();

    if current_branch == target {
        anyhow::bail!("Already on target branch '{}'", target);
    }

    // Find target worktree
    let target_wt = worktrees.iter()
        .find(|wt| wt.branch.as_deref() == Some(target))
        .ok_or_else(|| anyhow::anyhow!("Target worktree '{}' not found", target))?;
    let target_path = target_wt.path.clone();

    if opts.pr {
        // PR flow: push branch, create PR, clean up worktree
        let push = Command::new("git")
            .args(["push", "-u", "origin", &current_branch])
            .current_dir(&cwd)
            .status()?;
        if !push.success() {
            anyhow::bail!("Failed to push branch '{}'", current_branch);
        }

        // Check gh is available
        let gh_check = Command::new("gh")
            .arg("--version")
            .output();
        if gh_check.is_err() || !gh_check.unwrap().status.success() {
            anyhow::bail!("'gh' CLI not found. Install it: https://cli.github.com/");
        }

        // Create PR
        let pr_output = Command::new("gh")
            .args(["pr", "create", "--fill", "--base", target])
            .current_dir(&cwd)
            .output()?;
        if !pr_output.status.success() {
            let stderr = String::from_utf8_lossy(&pr_output.stderr);
            anyhow::bail!("Failed to create PR: {}", stderr.trim());
        }
        let url = String::from_utf8_lossy(&pr_output.stdout).trim().to_string();

        // Remove worktree (keep branch for PR)
        // We need to cd out first, so just prune the worktree without deleting branch
        let _ = Command::new("git")
            .args(["worktree", "remove", &cwd.display().to_string()])
            .current_dir(&target_path)
            .status();

        Ok(DoneResult::PrCreated { url, target_path })
    } else {
        // Merge flow: pull target, merge, push, clean up
        // Pull target if it has upstream
        if target_wt.ahead > 0 || target_wt.behind > 0 || target_wt.has_upstream() {
            let _ = Command::new("git")
                .args(["pull", "--rebase"])
                .current_dir(&target_path)
                .status();
        }

        // Merge current branch into target
        let merge_msg = format!("Merge branch '{}' into {}", current_branch, target);
        let merge = Command::new("git")
            .args(["merge", "--no-ff", &current_branch, "-m", &merge_msg])
            .current_dir(&target_path)
            .status()?;
        if !merge.success() {
            anyhow::bail!("Merge conflict. Resolve in {} and retry.", target_path.display());
        }

        // Push (unless --no-push)
        if !opts.no_push {
            let _ = Command::new("git")
                .args(["push"])
                .current_dir(&target_path)
                .status();
        }

        // Remove worktree + branch
        let _ = remove_worktree(&repo, &current_branch, true).await;

        Ok(DoneResult::Merged { target_path })
    }
}