git_workflow/commands/
new.rs1use crate::error::{GwError, Result};
17use crate::git;
18use crate::output;
19use crate::state::{RepoType, WorkingDirState};
20
21pub fn run(branch_name: Option<String>, stack: bool, verbose: bool) -> Result<()> {
23 if !git::is_git_repo() {
25 return Err(GwError::NotAGitRepository);
26 }
27
28 if git::is_detached_head() {
31 return Err(GwError::Other(
32 "Cannot run gw new from detached HEAD. Checkout a branch first.".to_string(),
33 ));
34 }
35
36 let branch_name = branch_name.ok_or(GwError::BranchNameRequired)?;
37
38 println!();
39 output::info(&format!("Creating branch: {}", output::bold(&branch_name)));
40
41 if git::branch_exists(&branch_name) {
43 output::error(&format!("Branch '{}' already exists locally", branch_name));
44 println!();
45 output::action(&format!(
46 "git checkout {} # Switch to existing branch",
47 branch_name
48 ));
49 output::action(&format!(
50 "git branch -d {} # Delete and recreate",
51 branch_name
52 ));
53 return Err(GwError::BranchAlreadyExists(branch_name));
54 }
55
56 let current = git::current_branch()?;
57 let repo_type = RepoType::detect()?;
58 let home_branch = repo_type.home_branch();
59 let on_home = current == home_branch;
60
61 if stack && on_home {
63 output::error(&format!(
66 "--stack requires a non-home branch, but you are on '{}'.",
67 current
68 ));
69 output::hints(&[
70 "gw new feature/your-feature # start fresh from origin/main",
71 "git checkout <parent> && gw new feature/child --stack # stack on a feature branch",
72 ]);
73 return Err(GwError::Other(
74 "--stack requires a non-home current branch".to_string(),
75 ));
76 }
77 if !stack && !on_home {
78 output::error(&format!(
82 "You are on '{}', not the home branch '{}'.",
83 current, home_branch
84 ));
85 output::hints(&[
86 &format!("gw new {branch_name} --stack # stack on {current}"),
87 &format!("gw home && gw new {branch_name} # start fresh from {home_branch}"),
88 ]);
89 return Err(GwError::Other(
90 "gw new outside the home branch needs --stack (or run gw home first)".to_string(),
91 ));
92 }
93
94 let working_dir = WorkingDirState::detect();
95 let dirty = !working_dir.is_clean();
96
97 let mut behind_count = 0usize;
105 let (start_point, base_label, pr_base): (String, String, Option<String>) = if stack {
106 (current.clone(), current.clone(), Some(current.clone()))
107 } else {
108 output::info("Fetching from origin...");
109 git::fetch_prune(verbose)?;
110 output::success("Fetched");
111 let default_remote = git::get_default_remote_branch()?;
112
113 if dirty {
114 behind_count = git::commit_count(¤t, &default_remote).unwrap_or(0);
115 (current.clone(), current.clone(), None)
116 } else {
117 (default_remote.clone(), default_remote, None)
118 }
119 };
120
121 if dirty {
123 output::warn(&format!(
124 "Working directory has changes ({}); they will move onto {}",
125 working_dir.description(),
126 output::bold(&branch_name)
127 ));
128 }
129
130 git::checkout_new_branch(&branch_name, &start_point, verbose)?;
133 output::success(&format!(
134 "Created branch {} from {}",
135 output::bold(&branch_name),
136 base_label
137 ));
138
139 if let Some(base) = &pr_base {
144 git::set_branch_base(&branch_name, base, verbose)?;
145 if let Ok(sha) = git::head_commit() {
149 git::set_branch_base_sha(&branch_name, &sha, verbose)?;
150 }
151 }
152
153 if behind_count > 0 {
154 output::warn(&format!(
155 "local {} is behind origin/{} ({} commit(s)); rebase after committing",
156 home_branch, home_branch, behind_count
157 ));
158 }
159
160 let commit_short = git::short_commit()?;
162 let commit_msg = git::head_commit_message()?;
163
164 output::ready("Ready to work", &branch_name);
165 println!("Base: {commit_short} {commit_msg}");
166
167 let mut hint_lines: Vec<String> = vec![
170 "# Make changes, then:".to_string(),
171 "git add <files> && git commit -m \"feat: description\"".to_string(),
172 ];
173 if behind_count > 0 {
174 hint_lines.push("gw sync # local main was behind; catch up".to_string());
175 }
176 hint_lines.push(format!("git push -u origin {branch_name}"));
177 hint_lines.push(match &pr_base {
178 Some(base) => format!("gh pr create -a \"@me\" -B {base} -t \"Title\""),
179 None => "gh pr create -a \"@me\" -t \"Title\"".to_string(),
180 });
181 let hint_refs: Vec<&str> = hint_lines.iter().map(String::as_str).collect();
182 output::hints(&hint_refs);
183
184 Ok(())
185}