git_workty/commands/
new.rs

1use crate::config::Config;
2use crate::git::{branch_exists, GitRepo};
3use crate::ui::{print_info, print_success};
4use crate::worktree::{list_worktrees, slug_from_branch};
5use anyhow::{bail, Context, Result};
6use std::path::PathBuf;
7use std::process::Command;
8
9pub struct NewOptions {
10    pub name: String,
11    pub from: Option<String>,
12    pub path: Option<PathBuf>,
13    pub print_path: bool,
14    pub open: bool,
15}
16
17pub fn execute(repo: &GitRepo, opts: NewOptions) -> Result<()> {
18    let config = Config::load(repo)?;
19
20    let branch_name = &opts.name;
21    let slug = slug_from_branch(branch_name);
22
23    let worktree_path = opts
24        .path
25        .unwrap_or_else(|| config.worktree_path(repo, &slug));
26
27    if worktree_path.exists() {
28        bail!(
29            "Directory already exists: {}\nUse --path to specify a different location.",
30            worktree_path.display()
31        );
32    }
33
34    let existing = list_worktrees(repo)?;
35    if let Some(existing_wt) = existing
36        .iter()
37        .find(|wt| wt.branch_short.as_deref() == Some(branch_name))
38    {
39        bail!(
40            "Branch '{}' is already checked out at: {}\nUse `git workty go {}` to switch to it.",
41            branch_name,
42            existing_wt.path.display(),
43            branch_name
44        );
45    }
46
47    let mut base = opts.from.unwrap_or_else(|| config.base.clone());
48
49    if let Some(parent) = worktree_path.parent() {
50        std::fs::create_dir_all(parent)
51            .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
52    }
53
54    let branch_already_exists = branch_exists(repo, branch_name);
55
56    if branch_already_exists {
57        print_info(&format!("Using existing branch '{}'", branch_name));
58
59        let output = Command::new("git")
60            .current_dir(&repo.root)
61            .args([
62                "worktree",
63                "add",
64                worktree_path.to_str().unwrap(),
65                branch_name,
66            ])
67            .output()
68            .context("Failed to create worktree")?;
69
70        if !output.status.success() {
71            let stderr = String::from_utf8_lossy(&output.stderr);
72            bail!("Failed to create worktree: {}", stderr.trim());
73        }
74    } else {
75        // Try to fetch upstream of base to ensure we are up to date
76        if let Some(upstream) = get_upstream(repo, &base) {
77            print_info(&format!("Fetching {} to ensure fresh start...", upstream));
78
79            // upstream is likely "origin/main", we want to split to "origin" "main"
80            if let Some((remote, branch)) = upstream.split_once('/') {
81                let _ = Command::new("git")
82                    .current_dir(&repo.root)
83                    .args(["fetch", remote, branch])
84                    .output();
85
86                // Update base to use the upstream ref (e.g. origin/main)
87                // so we branch off the latest remote commit
88                base = upstream;
89            }
90        }
91
92        print_info(&format!(
93            "Creating new branch '{}' from '{}'",
94            branch_name, base
95        ));
96
97        let output = Command::new("git")
98            .current_dir(&repo.root)
99            .args([
100                "worktree",
101                "add",
102                "-b",
103                branch_name,
104                worktree_path.to_str().unwrap(),
105                &base,
106            ])
107            .output()
108            .context("Failed to create worktree")?;
109
110        if !output.status.success() {
111            let stderr = String::from_utf8_lossy(&output.stderr);
112            bail!("Failed to create worktree: {}", stderr.trim());
113        }
114
115        // Try to set upstream
116        print_info("Setting upstream...");
117        let push_res = Command::new("git")
118            .current_dir(&repo.root)
119            .args(["push", "-u", "origin", branch_name])
120            .output();
121
122        match push_res {
123            Ok(p) if p.status.success() => {
124                print_success("Upstream set successfully");
125            }
126            Ok(p) => {
127                let stderr = String::from_utf8_lossy(&p.stderr);
128                print_info(&format!(
129                    "Note: Could not set upstream (remote might not exist yet?)\nGit error: {}",
130                    stderr.trim()
131                ));
132            }
133            Err(_) => {
134                print_info("Note: Could not run git push to set upstream");
135            }
136        }
137    }
138
139    if opts.print_path {
140        println!("{}", worktree_path.display());
141    } else {
142        print_success(&format!("Created worktree at {}", worktree_path.display()));
143    }
144
145    if opts.open {
146        if let Some(open_cmd) = &config.open_cmd {
147            let _ = Command::new(open_cmd).arg(&worktree_path).spawn();
148        }
149    }
150
151    Ok(())
152}
153
154fn get_upstream(repo: &GitRepo, branch: &str) -> Option<String> {
155    let output = Command::new("git")
156        .current_dir(&repo.root)
157        .args([
158            "rev-parse",
159            "--abbrev-ref",
160            "--symbolic-full-name",
161            &format!("{}@{{u}}", branch),
162        ])
163        .output()
164        .ok()?;
165
166    if output.status.success() {
167        let s = String::from_utf8_lossy(&output.stdout).trim().to_string();
168        if s.is_empty() {
169            None
170        } else {
171            Some(s)
172        }
173    } else {
174        None
175    }
176}