Skip to main content

thor_wt/
done.rs

1use thor_core::{find_repo, list_worktrees, remove_worktree};
2use std::path::PathBuf;
3use std::process::Command;
4
5/// Options for the done command.
6pub struct DoneOpts {
7    pub no_push: bool,
8    pub pr: bool,
9}
10
11/// Result of a done operation.
12pub enum DoneResult {
13    Merged { target_path: PathBuf },
14    PrCreated { url: String, target_path: PathBuf },
15}
16
17/// Finish the current branch: merge into target or create a PR, then clean up.
18pub async fn done(target: &str, opts: &DoneOpts) -> anyhow::Result<DoneResult> {
19    let repo = find_repo()?;
20    let worktrees = list_worktrees(&repo).await?;
21    let cwd = std::env::current_dir()?;
22
23    // Find current branch
24    let current_wt = worktrees.iter()
25        .find(|wt| cwd.starts_with(&wt.path))
26        .ok_or_else(|| anyhow::anyhow!("Not in a worktree"))?;
27
28    let current_branch = current_wt.branch.as_ref()
29        .ok_or_else(|| anyhow::anyhow!("Current worktree has no branch (detached HEAD)"))?
30        .clone();
31
32    if current_branch == target {
33        anyhow::bail!("Already on target branch '{}'", target);
34    }
35
36    // Find target worktree
37    let target_wt = worktrees.iter()
38        .find(|wt| wt.branch.as_deref() == Some(target))
39        .ok_or_else(|| anyhow::anyhow!("Target worktree '{}' not found", target))?;
40    let target_path = target_wt.path.clone();
41
42    if opts.pr {
43        // PR flow: push branch, create PR, clean up worktree
44        let push = Command::new("git")
45            .args(["push", "-u", "origin", &current_branch])
46            .current_dir(&cwd)
47            .status()?;
48        if !push.success() {
49            anyhow::bail!("Failed to push branch '{}'", current_branch);
50        }
51
52        // Check gh is available
53        let gh_check = Command::new("gh")
54            .arg("--version")
55            .output();
56        if gh_check.is_err() || !gh_check.unwrap().status.success() {
57            anyhow::bail!("'gh' CLI not found. Install it: https://cli.github.com/");
58        }
59
60        // Create PR
61        let pr_output = Command::new("gh")
62            .args(["pr", "create", "--fill", "--base", target])
63            .current_dir(&cwd)
64            .output()?;
65        if !pr_output.status.success() {
66            let stderr = String::from_utf8_lossy(&pr_output.stderr);
67            anyhow::bail!("Failed to create PR: {}", stderr.trim());
68        }
69        let url = String::from_utf8_lossy(&pr_output.stdout).trim().to_string();
70
71        // Remove worktree (keep branch for PR)
72        // We need to cd out first, so just prune the worktree without deleting branch
73        let _ = Command::new("git")
74            .args(["worktree", "remove", &cwd.display().to_string()])
75            .current_dir(&target_path)
76            .status();
77
78        Ok(DoneResult::PrCreated { url, target_path })
79    } else {
80        // Merge flow: pull target, merge, push, clean up
81        // Pull target if it has upstream
82        if target_wt.ahead > 0 || target_wt.behind > 0 || target_wt.has_upstream() {
83            let _ = Command::new("git")
84                .args(["pull", "--rebase"])
85                .current_dir(&target_path)
86                .status();
87        }
88
89        // Merge current branch into target
90        let merge_msg = format!("Merge branch '{}' into {}", current_branch, target);
91        let merge = Command::new("git")
92            .args(["merge", "--no-ff", &current_branch, "-m", &merge_msg])
93            .current_dir(&target_path)
94            .status()?;
95        if !merge.success() {
96            anyhow::bail!("Merge conflict. Resolve in {} and retry.", target_path.display());
97        }
98
99        // Push (unless --no-push)
100        if !opts.no_push {
101            let _ = Command::new("git")
102                .args(["push"])
103                .current_dir(&target_path)
104                .status();
105        }
106
107        // Remove worktree + branch
108        let _ = remove_worktree(&repo, &current_branch, true).await;
109
110        Ok(DoneResult::Merged { target_path })
111    }
112}