git_workty/commands/
new.rs1use 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 if let Some(upstream) = get_upstream(repo, &base) {
77 print_info(&format!("Fetching {} to ensure fresh start...", upstream));
78
79 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 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 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}