1use crate::{GitXError, Result};
2use std::process::Command;
3
4pub struct GitOperations;
6
7impl GitOperations {
8 pub fn run(args: &[&str]) -> Result<String> {
10 let output = Command::new("git").args(args).output()?;
11
12 if output.status.success() {
13 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
14 } else {
15 let stderr_output = String::from_utf8_lossy(&output.stderr);
16 let stderr = stderr_output.trim();
17 Err(GitXError::GitCommand(stderr.to_string()))
18 }
19 }
20
21 pub fn run_status(args: &[&str]) -> Result<()> {
23 let status = Command::new("git").args(args).status()?;
24
25 if status.success() {
26 Ok(())
27 } else {
28 Err(GitXError::GitCommand(format!(
29 "Git command failed: git {}",
30 args.join(" ")
31 )))
32 }
33 }
34
35 pub fn current_branch() -> Result<String> {
37 Self::run(&["rev-parse", "--abbrev-ref", "HEAD"])
38 }
39
40 pub fn repo_root() -> Result<String> {
42 Self::run(&["rev-parse", "--show-toplevel"])
43 }
44
45 pub fn commit_exists(commit: &str) -> Result<bool> {
47 match Self::run(&["rev-parse", "--verify", &format!("{commit}^{{commit}}")]) {
48 Ok(_) => Ok(true),
49 Err(GitXError::GitCommand(_)) => Ok(false),
50 Err(e) => Err(e),
51 }
52 }
53
54 pub fn short_hash(commit: &str) -> Result<String> {
56 Self::run(&["rev-parse", "--short", commit])
57 }
58
59 pub fn upstream_branch() -> Result<String> {
61 Self::run(&["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"])
62 }
63
64 pub fn ahead_behind_counts() -> Result<(u32, u32)> {
66 let output = Self::run(&["rev-list", "--left-right", "--count", "HEAD...@{u}"])?;
67 let mut parts = output.split_whitespace();
68 let ahead = parts.next().unwrap_or("0").parse().unwrap_or(0);
69 let behind = parts.next().unwrap_or("0").parse().unwrap_or(0);
70 Ok((ahead, behind))
71 }
72
73 pub fn branch_info_optimized() -> Result<(String, Option<String>, u32, u32)> {
75 let current = Self::current_branch()?;
77
78 match Self::upstream_branch() {
80 Ok(upstream) => {
81 let (ahead, behind) = Self::ahead_behind_counts().unwrap_or((0, 0));
83 Ok((current, Some(upstream), ahead, behind))
84 }
85 Err(_) => {
86 Ok((current, None, 0, 0))
88 }
89 }
90 }
91
92 pub fn local_branches() -> Result<Vec<String>> {
94 let output = Self::run(&["branch", "--format=%(refname:short)"])?;
95 let branches: Vec<String> = output
96 .lines()
97 .map(|line| line.trim().to_string())
98 .filter(|branch| !branch.is_empty())
99 .collect();
100 Ok(branches)
101 }
102
103 pub fn recent_branches(limit: Option<usize>) -> Result<Vec<String>> {
105 let output = Self::run(&[
106 "for-each-ref",
107 "--sort=-committerdate",
108 "--format=%(refname:short)",
109 "refs/heads/",
110 ])?;
111
112 let current_branch = Self::current_branch().unwrap_or_default();
113 let mut branches: Vec<String> = output
114 .lines()
115 .map(|s| s.trim().to_string())
116 .filter(|branch| !branch.is_empty() && branch != ¤t_branch)
117 .collect();
118
119 if let Some(limit) = limit {
120 branches.truncate(limit);
121 }
122
123 Ok(branches)
124 }
125
126 pub fn merged_branches() -> Result<Vec<String>> {
128 let output = Self::run(&["branch", "--merged"])?;
129 let branches: Vec<String> = output
130 .lines()
131 .map(|line| line.trim().trim_start_matches("* ").to_string())
132 .filter(|branch| !branch.is_empty())
133 .collect();
134 Ok(branches)
135 }
136
137 pub fn is_working_directory_clean() -> Result<bool> {
139 let output = Self::run(&["status", "--porcelain"])?;
140 Ok(output.trim().is_empty())
141 }
142
143 pub fn staged_files() -> Result<Vec<String>> {
145 let output = Self::run(&["diff", "--cached", "--name-only"])?;
146 let files: Vec<String> = output
147 .lines()
148 .map(|line| line.trim().to_string())
149 .filter(|file| !file.is_empty())
150 .collect();
151 Ok(files)
152 }
153}
154
155pub struct BranchOperations;
157
158impl BranchOperations {
159 pub fn create(name: &str, from: Option<&str>) -> Result<()> {
161 let mut args = vec!["checkout", "-b", name];
162 if let Some(base) = from {
163 args.push(base);
164 }
165 GitOperations::run_status(&args)
166 }
167
168 pub fn delete(name: &str, force: bool) -> Result<()> {
170 let flag = if force { "-D" } else { "-d" };
171 GitOperations::run_status(&["branch", flag, name])
172 }
173
174 pub fn rename(new_name: &str) -> Result<()> {
176 GitOperations::run_status(&["branch", "-m", new_name])
177 }
178
179 pub fn switch(name: &str) -> Result<()> {
181 GitOperations::run_status(&["checkout", name])
182 }
183
184 pub fn exists(name: &str) -> Result<bool> {
186 match GitOperations::run(&["rev-parse", "--verify", &format!("refs/heads/{name}")]) {
187 Ok(_) => Ok(true),
188 Err(GitXError::GitCommand(_)) => Ok(false),
189 Err(e) => Err(e),
190 }
191 }
192}
193
194pub struct CommitOperations;
196
197impl CommitOperations {
198 pub fn fixup(commit_hash: &str) -> Result<()> {
200 GitOperations::run_status(&["commit", "--fixup", commit_hash])
201 }
202
203 pub fn undo_last() -> Result<()> {
205 GitOperations::run_status(&["reset", "--soft", "HEAD~1"])
206 }
207
208 pub fn get_message(commit_hash: &str) -> Result<String> {
210 GitOperations::run(&["log", "-1", "--pretty=format:%s", commit_hash])
211 }
212
213 pub fn get_author(commit_hash: &str) -> Result<String> {
215 GitOperations::run(&["log", "-1", "--pretty=format:%an <%ae>", commit_hash])
216 }
217}
218
219pub struct RemoteOperations;
221
222impl RemoteOperations {
223 pub fn set_upstream(remote: &str, branch: &str) -> Result<()> {
225 GitOperations::run_status(&["branch", "--set-upstream-to", &format!("{remote}/{branch}")])
226 }
227
228 pub fn push(remote: Option<&str>, branch: Option<&str>) -> Result<()> {
230 let mut args = vec!["push"];
231 if let Some(r) = remote {
232 args.push(r);
233 }
234 if let Some(b) = branch {
235 args.push(b);
236 }
237 GitOperations::run_status(&args)
238 }
239
240 pub fn fetch(remote: Option<&str>) -> Result<()> {
242 let mut args = vec!["fetch"];
243 if let Some(r) = remote {
244 args.push(r);
245 }
246 GitOperations::run_status(&args)
247 }
248
249 pub fn list() -> Result<Vec<String>> {
251 let output = GitOperations::run(&["remote"])?;
252 let remotes: Vec<String> = output
253 .lines()
254 .map(|line| line.trim().to_string())
255 .filter(|remote| !remote.is_empty())
256 .collect();
257 Ok(remotes)
258 }
259}