1use std::collections::HashMap;
5use std::process::Command;
6
7use console::style;
8
9use crate::constants::{
10 format_config_key, CONFIG_KEY_BASE_BRANCH, CONFIG_KEY_BASE_PATH, CONFIG_KEY_INTENDED_BRANCH,
11};
12use crate::error::{CwError, Result};
13use crate::git;
14use crate::hooks;
15use crate::registry;
16
17use super::helpers::{get_worktree_metadata, resolve_worktree_target};
18
19pub fn create_pr_worktree(
21 target: Option<&str>,
22 push: bool,
23 title: Option<&str>,
24 body: Option<&str>,
25 draft: bool,
26) -> Result<()> {
27 if !git::has_command("gh") {
28 return Err(CwError::Git(
29 "GitHub CLI (gh) is required to create pull requests.\n\
30 Install it from: https://cli.github.com/"
31 .to_string(),
32 ));
33 }
34
35 let (cwd, feature_branch, worktree_repo) = resolve_worktree_target(target, None)?;
36 let (base_branch, base_path) = get_worktree_metadata(&feature_branch, &worktree_repo)?;
37
38 println!("\n{}", style("Creating Pull Request:").cyan().bold());
39 println!(" Feature: {}", style(&feature_branch).green());
40 println!(" Base: {}", style(&base_branch).green());
41 println!(" Repo: {}\n", style(base_path.display()).blue());
42
43 let mut hook_ctx = HashMap::new();
45 hook_ctx.insert("branch".into(), feature_branch.clone());
46 hook_ctx.insert("base_branch".into(), base_branch.clone());
47 hook_ctx.insert("worktree_path".into(), cwd.to_string_lossy().to_string());
48 hook_ctx.insert("repo_path".into(), base_path.to_string_lossy().to_string());
49 hook_ctx.insert("event".into(), "pr.pre".into());
50 hook_ctx.insert("operation".into(), "pr".into());
51 hooks::run_hooks("pr.pre", &hook_ctx, Some(&cwd), Some(&base_path))?;
52
53 println!("{}", style("Fetching updates from remote...").yellow());
55 let fetch_ok = git::git_command(
56 &["fetch", "--all", "--prune"],
57 Some(&base_path),
58 false,
59 true,
60 )
61 .map(|r| r.returncode == 0)
62 .unwrap_or(false);
63
64 let rebase_target = if fetch_ok {
66 let origin_ref = format!("origin/{}", base_branch);
67 if git::branch_exists(&origin_ref, Some(&cwd)) {
68 origin_ref
69 } else {
70 base_branch.clone()
71 }
72 } else {
73 base_branch.clone()
74 };
75
76 println!(
78 "{}",
79 style(format!(
80 "Rebasing {} onto {}...",
81 feature_branch, rebase_target
82 ))
83 .yellow()
84 );
85
86 match git::git_command(&["rebase", &rebase_target], Some(&cwd), false, true) {
87 Ok(r) if r.returncode == 0 => {}
88 _ => {
89 let conflicts = git::git_command(
91 &["diff", "--name-only", "--diff-filter=U"],
92 Some(&cwd),
93 false,
94 true,
95 )
96 .ok()
97 .and_then(|r| {
98 if r.returncode == 0 && !r.stdout.trim().is_empty() {
99 Some(r.stdout.trim().to_string())
100 } else {
101 None
102 }
103 });
104
105 let _ = git::git_command(&["rebase", "--abort"], Some(&cwd), false, false);
106
107 let mut msg = format!(
108 "Rebase failed. Please resolve conflicts manually:\n cd {}\n git rebase {}",
109 cwd.display(),
110 rebase_target
111 );
112 if let Some(files) = conflicts {
113 msg.push_str("\n\nConflicted files:\n");
114 for f in files.lines() {
115 msg.push_str(&format!(" - {}\n", f));
116 }
117 }
118 return Err(CwError::Rebase(msg));
119 }
120 }
121
122 println!("{} Rebase successful\n", style("*").green().bold());
123
124 if push {
126 println!(
127 "{}",
128 style(format!("Pushing {} to origin...", feature_branch)).yellow()
129 );
130 match git::git_command(
131 &["push", "-u", "origin", &feature_branch],
132 Some(&cwd),
133 false,
134 true,
135 ) {
136 Ok(r) if r.returncode == 0 => {
137 println!("{} Pushed to origin\n", style("*").green().bold());
138 }
139 Ok(r) => {
140 match git::git_command(
142 &[
143 "push",
144 "--force-with-lease",
145 "-u",
146 "origin",
147 &feature_branch,
148 ],
149 Some(&cwd),
150 false,
151 true,
152 ) {
153 Ok(r2) if r2.returncode == 0 => {
154 println!("{} Force pushed to origin\n", style("*").green().bold());
155 }
156 _ => {
157 return Err(CwError::Git(format!("Push failed: {}", r.stdout)));
158 }
159 }
160 }
161 Err(e) => return Err(e),
162 }
163 }
164
165 println!("{}", style("Creating pull request...").yellow());
167
168 let mut pr_args = vec![
169 "gh".to_string(),
170 "pr".to_string(),
171 "create".to_string(),
172 "--base".to_string(),
173 base_branch.clone(),
174 ];
175
176 if let Some(t) = title {
177 pr_args.extend(["--title".to_string(), t.to_string()]);
178 if let Some(b) = body {
179 pr_args.extend(["--body".to_string(), b.to_string()]);
180 }
181 } else {
182 pr_args.push("--fill".to_string());
183 }
184
185 if draft {
186 pr_args.push("--draft".to_string());
187 }
188
189 let output = Command::new(&pr_args[0])
190 .args(&pr_args[1..])
191 .current_dir(&cwd)
192 .output()?;
193
194 if output.status.success() {
195 let pr_url = String::from_utf8_lossy(&output.stdout).trim().to_string();
196 println!("{} Pull request created!\n", style("*").green().bold());
197 println!("{} {}\n", style("PR URL:").bold(), pr_url);
198 println!(
199 "{}\n",
200 style("Note: Worktree is still active. Use 'gw delete' to remove after PR is merged.")
201 .dim()
202 );
203
204 hook_ctx.insert("event".into(), "pr.post".into());
206 hook_ctx.insert("pr_url".into(), pr_url);
207 let _ = hooks::run_hooks("pr.post", &hook_ctx, Some(&cwd), Some(&base_path));
208 } else {
209 let stderr = String::from_utf8_lossy(&output.stderr);
210 return Err(CwError::Git(format!(
211 "Failed to create pull request: {}",
212 stderr
213 )));
214 }
215
216 Ok(())
217}
218
219pub fn merge_worktree(
221 target: Option<&str>,
222 push: bool,
223 _interactive: bool,
224 dry_run: bool,
225) -> Result<()> {
226 let (cwd, feature_branch, worktree_repo) = resolve_worktree_target(target, None)?;
227 let (base_branch, base_path) = get_worktree_metadata(&feature_branch, &worktree_repo)?;
228 let repo = &base_path;
229
230 println!("\n{}", style("Finishing worktree:").cyan().bold());
231 println!(" Feature: {}", style(&feature_branch).green());
232 println!(" Base: {}", style(&base_branch).green());
233 println!(" Repo: {}\n", style(repo.display()).blue());
234
235 let mut hook_ctx = HashMap::new();
237 hook_ctx.insert("branch".into(), feature_branch.clone());
238 hook_ctx.insert("base_branch".into(), base_branch.clone());
239 hook_ctx.insert("worktree_path".into(), cwd.to_string_lossy().to_string());
240 hook_ctx.insert("repo_path".into(), repo.to_string_lossy().to_string());
241 hook_ctx.insert("event".into(), "merge.pre".into());
242 hook_ctx.insert("operation".into(), "merge".into());
243 if !dry_run {
244 hooks::run_hooks("merge.pre", &hook_ctx, Some(&cwd), Some(repo))?;
245 }
246
247 if dry_run {
249 println!(
250 "{}\n",
251 style("DRY RUN MODE — No changes will be made")
252 .yellow()
253 .bold()
254 );
255 println!(
256 "{}\n",
257 style("The following operations would be performed:").bold()
258 );
259 println!(" 1. Fetch updates from remote");
260 println!(" 2. Rebase {} onto {}", feature_branch, base_branch);
261 println!(" 3. Switch to {} in base repository", base_branch);
262 println!(
263 " 4. Merge {} into {} (fast-forward)",
264 feature_branch, base_branch
265 );
266 if push {
267 println!(" 5. Push {} to origin", base_branch);
268 println!(" 6. Remove worktree at {}", cwd.display());
269 println!(" 7. Delete local branch {}", feature_branch);
270 } else {
271 println!(" 5. Remove worktree at {}", cwd.display());
272 println!(" 6. Delete local branch {}", feature_branch);
273 }
274 println!("\n{}\n", style("Run without --dry-run to execute.").dim());
275 return Ok(());
276 }
277
278 let fetch_ok = git::git_command(&["fetch", "--all", "--prune"], Some(repo), false, true)
280 .map(|r| r.returncode == 0)
281 .unwrap_or(false);
282
283 let rebase_target = if fetch_ok {
284 let origin_ref = format!("origin/{}", base_branch);
285 if git::branch_exists(&origin_ref, Some(&cwd)) {
286 origin_ref
287 } else {
288 base_branch.clone()
289 }
290 } else {
291 base_branch.clone()
292 };
293
294 println!(
296 "{}",
297 style(format!(
298 "Rebasing {} onto {}...",
299 feature_branch, rebase_target
300 ))
301 .yellow()
302 );
303
304 match git::git_command(&["rebase", &rebase_target], Some(&cwd), false, true) {
305 Ok(r) if r.returncode == 0 => {}
306 _ => {
307 let _ = git::git_command(&["rebase", "--abort"], Some(&cwd), false, false);
308 return Err(CwError::Rebase(format!(
309 "Rebase failed. Resolve conflicts manually:\n cd {}\n git rebase {}",
310 cwd.display(),
311 rebase_target
312 )));
313 }
314 }
315
316 println!("{} Rebase successful\n", style("*").green().bold());
317
318 if !base_path.exists() {
320 return Err(CwError::WorktreeNotFound(format!(
321 "Base repository not found at: {}",
322 base_path.display()
323 )));
324 }
325
326 println!(
328 "{}",
329 style(format!(
330 "Merging {} into {}...",
331 feature_branch, base_branch
332 ))
333 .yellow()
334 );
335
336 let _ = git::git_command(
338 &["fetch", "--all", "--prune"],
339 Some(&base_path),
340 false,
341 false,
342 );
343 if let Ok(current) = git::get_current_branch(Some(&base_path)) {
344 if current != base_branch {
345 git::git_command(&["switch", &base_branch], Some(&base_path), true, false)?;
346 }
347 } else {
348 git::git_command(&["switch", &base_branch], Some(&base_path), true, false)?;
349 }
350
351 match git::git_command(
352 &["merge", "--ff-only", &feature_branch],
353 Some(&base_path),
354 false,
355 true,
356 ) {
357 Ok(r) if r.returncode == 0 => {}
358 _ => {
359 return Err(CwError::Merge(format!(
360 "Fast-forward merge failed. Manual intervention required:\n cd {}\n git merge {}",
361 base_path.display(),
362 feature_branch
363 )));
364 }
365 }
366
367 println!(
368 "{} Merged {} into {}\n",
369 style("*").green().bold(),
370 feature_branch,
371 base_branch
372 );
373
374 if push {
376 println!(
377 "{}",
378 style(format!("Pushing {} to origin...", base_branch)).yellow()
379 );
380 match git::git_command(
381 &["push", "origin", &base_branch],
382 Some(&base_path),
383 false,
384 true,
385 ) {
386 Ok(r) if r.returncode == 0 => {
387 println!("{} Pushed to origin\n", style("*").green().bold());
388 }
389 _ => {
390 println!("{} Push failed\n", style("!").yellow());
391 }
392 }
393 }
394
395 println!("{}", style("Cleaning up worktree and branch...").yellow());
397
398 let _ = std::env::set_current_dir(repo);
399
400 git::remove_worktree_safe(&cwd, repo, true)?;
401 let _ = git::git_command(&["branch", "-D", &feature_branch], Some(repo), false, false);
402
403 let bb_key = format_config_key(CONFIG_KEY_BASE_BRANCH, &feature_branch);
405 let bp_key = format_config_key(CONFIG_KEY_BASE_PATH, &feature_branch);
406 let ib_key = format_config_key(CONFIG_KEY_INTENDED_BRANCH, &feature_branch);
407 git::unset_config(&bb_key, Some(repo));
408 git::unset_config(&bp_key, Some(repo));
409 git::unset_config(&ib_key, Some(repo));
410
411 println!("{}\n", style("* Cleanup complete!").green().bold());
412
413 hook_ctx.insert("event".into(), "merge.post".into());
415 let _ = hooks::run_hooks("merge.post", &hook_ctx, Some(repo), Some(repo));
416 let _ = registry::update_last_seen(repo);
417
418 Ok(())
419}