1use std::process::Command;
4
5use crate::error::{GwError, Result};
6use crate::output;
7
8fn git_run(args: &[&str], verbose: bool) -> Result<()> {
10 if verbose {
11 output::action(&format!("git {}", args.join(" ")));
12 }
13
14 let output = Command::new("git")
15 .args(args)
16 .output()
17 .map_err(|e| GwError::GitCommandFailed(format!("Failed to execute git: {e}")))?;
18
19 if output.status.success() {
20 Ok(())
21 } else {
22 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
23 Err(GwError::GitCommandFailed(stderr))
24 }
25}
26
27fn git_output(args: &[&str], verbose: bool) -> Result<String> {
29 if verbose {
30 output::action(&format!("git {}", args.join(" ")));
31 }
32
33 let output = Command::new("git")
34 .args(args)
35 .output()
36 .map_err(|e| GwError::GitCommandFailed(format!("Failed to execute git: {e}")))?;
37
38 if output.status.success() {
39 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
40 } else {
41 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
42 Err(GwError::GitCommandFailed(stderr))
43 }
44}
45
46pub fn fetch_prune(verbose: bool) -> Result<()> {
48 git_run(&["fetch", "--prune", "--quiet"], verbose)
49}
50
51pub fn checkout(branch: &str, verbose: bool) -> Result<()> {
53 git_run(&["checkout", branch, "--quiet"], verbose).map_err(|e| map_checkout_error(branch, e))
54}
55
56pub fn checkout_new_branch(branch: &str, start_point: &str, verbose: bool) -> Result<()> {
58 git_run(&["checkout", "-b", branch, start_point, "--quiet"], verbose)
59 .map_err(|e| map_checkout_error(branch, e))
60}
61
62fn map_checkout_error(branch: &str, err: GwError) -> GwError {
68 let GwError::GitCommandFailed(ref msg) = err else {
69 return err;
70 };
71 if msg.contains("already checked out") || msg.contains("already used by worktree") {
72 return GwError::BranchCheckedOutElsewhere {
73 branch: branch.to_string(),
74 path: extract_worktree_path(msg),
75 };
76 }
77 err
78}
79
80fn extract_worktree_path(msg: &str) -> Option<String> {
83 let start = msg.find("at '")? + "at '".len();
84 let rest = &msg[start..];
85 let end = rest.find('\'')?;
86 Some(rest[..end].to_string())
87}
88
89pub fn pull_ff_only(remote: &str, branch: &str, verbose: bool) -> Result<()> {
94 git_run(&["pull", remote, branch, "--ff-only", "--quiet"], verbose)
95}
96
97pub fn force_update_branch(branch: &str, target: &str, verbose: bool) -> Result<()> {
104 git_run(&["branch", "-f", branch, target], verbose)
105}
106
107pub fn delete_branch(branch: &str, verbose: bool) -> Result<()> {
109 git_run(&["branch", "-d", branch], verbose)
110}
111
112pub fn force_delete_branch(branch: &str, verbose: bool) -> Result<()> {
114 git_run(&["branch", "-D", branch], verbose)
115}
116
117#[allow(dead_code)]
119pub fn delete_remote_branch(branch: &str, verbose: bool) -> Result<()> {
120 git_run(&["push", "origin", "--delete", branch], verbose)
121}
122
123pub fn log_commits(from: &str, to: &str, verbose: bool) -> Result<Vec<String>> {
125 let output = git_output(&["log", &format!("{from}..{to}"), "--oneline"], verbose)?;
126 Ok(output.lines().map(String::from).collect())
127}
128
129pub fn add_all(verbose: bool) -> Result<()> {
131 git_run(&["add", "-A"], verbose)
132}
133
134pub fn commit(message: &str, verbose: bool) -> Result<()> {
136 git_run(&["commit", "-m", message], verbose)
137}
138
139pub fn reset_soft(target: &str, verbose: bool) -> Result<()> {
141 git_run(&["reset", "--soft", target], verbose)
142}
143
144pub fn discard_all_changes(verbose: bool) -> Result<()> {
146 git_run(&["reset", "--hard", "HEAD"], verbose)?;
148 git_run(&["clean", "-fd"], verbose)
150}
151
152pub fn rebase(target: &str, verbose: bool) -> Result<()> {
154 git_run(&["rebase", target], verbose)
155}
156
157pub fn rebase_onto(new_base: &str, old_base: &str, verbose: bool) -> Result<()> {
165 git_run(&["rebase", "--onto", new_base, old_base], verbose)
166}
167
168pub fn force_push_with_lease(branch: &str, verbose: bool) -> Result<()> {
170 git_run(&["push", "--force-with-lease", "origin", branch], verbose)
171}
172
173pub fn set_branch_base(branch: &str, base: &str, verbose: bool) -> Result<()> {
180 git_run(
181 &["config", &format!("branch.{branch}.gwBase"), base],
182 verbose,
183 )
184}
185
186pub fn set_branch_base_sha(branch: &str, sha: &str, verbose: bool) -> Result<()> {
190 git_run(
191 &["config", &format!("branch.{branch}.gwBaseSha"), sha],
192 verbose,
193 )
194}
195
196pub fn unset_branch_base(branch: &str, verbose: bool) -> Result<()> {
202 unset_config_key(&format!("branch.{branch}.gwBase"), verbose)?;
203 unset_config_key(&format!("branch.{branch}.gwBaseSha"), verbose)
204}
205
206fn unset_config_key(key: &str, verbose: bool) -> Result<()> {
208 if verbose {
209 output::action(&format!("git config --unset {key}"));
210 }
211 let output = Command::new("git")
212 .args(["config", "--unset", key])
213 .output()
214 .map_err(|e| GwError::GitCommandFailed(format!("Failed to execute git: {e}")))?;
215 match output.status.code() {
217 Some(0) | Some(5) => Ok(()),
218 _ => {
219 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
220 Err(GwError::GitCommandFailed(stderr))
221 }
222 }
223}
224
225pub fn worktree_add(path: &str, branch: &str, start_point: &str, verbose: bool) -> Result<()> {
227 git_run(
228 &["worktree", "add", "-b", branch, path, start_point],
229 verbose,
230 )
231}
232
233pub fn worktree_remove(path: &str, verbose: bool) -> Result<()> {
235 git_run(&["worktree", "remove", "--force", path], verbose)
236}
237
238pub fn worktree_prune(verbose: bool) -> Result<()> {
240 git_run(&["worktree", "prune"], verbose)
241}
242
243pub fn git_run_in_dir(dir: &str, args: &[&str], verbose: bool) -> Result<()> {
247 if verbose {
248 output::action(&format!("git -C {} {}", dir, args.join(" ")));
249 }
250
251 let output = Command::new("git")
252 .args(args)
253 .current_dir(dir)
254 .output()
255 .map_err(|e| GwError::GitCommandFailed(format!("Failed to execute git: {e}")))?;
256
257 if output.status.success() {
258 Ok(())
259 } else {
260 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
261 Err(GwError::GitCommandFailed(stderr))
262 }
263}