Skip to main content

git_workty/commands/
new.rs

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