git_workty/commands/
new.rs1use 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 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 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 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 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}