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 AsyncGitOperations;
157
158impl AsyncGitOperations {
159 pub async fn run(args: &[&str]) -> Result<String> {
161 let output = tokio::process::Command::new("git")
162 .args(args)
163 .output()
164 .await?;
165
166 if output.status.success() {
167 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
168 } else {
169 let stderr_output = String::from_utf8_lossy(&output.stderr);
170 let stderr = stderr_output.trim();
171 Err(GitXError::GitCommand(stderr.to_string()))
172 }
173 }
174
175 pub async fn run_status(args: &[&str]) -> Result<()> {
177 let status = tokio::process::Command::new("git")
178 .args(args)
179 .status()
180 .await?;
181
182 if status.success() {
183 Ok(())
184 } else {
185 Err(GitXError::GitCommand(format!(
186 "Git command failed: git {}",
187 args.join(" ")
188 )))
189 }
190 }
191
192 pub async fn current_branch() -> Result<String> {
194 Self::run(&["rev-parse", "--abbrev-ref", "HEAD"]).await
195 }
196
197 pub async fn repo_root() -> Result<String> {
199 Self::run(&["rev-parse", "--show-toplevel"]).await
200 }
201
202 pub async fn commit_exists(commit: &str) -> Result<bool> {
204 match Self::run(&["rev-parse", "--verify", &format!("{commit}^{{commit}}")]).await {
205 Ok(_) => Ok(true),
206 Err(GitXError::GitCommand(_)) => Ok(false),
207 Err(e) => Err(e),
208 }
209 }
210
211 pub async fn short_hash(commit: &str) -> Result<String> {
213 Self::run(&["rev-parse", "--short", commit]).await
214 }
215
216 pub async fn upstream_branch() -> Result<String> {
218 Self::run(&["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]).await
219 }
220
221 pub async fn ahead_behind_counts() -> Result<(u32, u32)> {
223 let output = Self::run(&["rev-list", "--left-right", "--count", "HEAD...@{u}"]).await?;
224 let mut parts = output.split_whitespace();
225 let ahead = parts.next().unwrap_or("0").parse().unwrap_or(0);
226 let behind = parts.next().unwrap_or("0").parse().unwrap_or(0);
227 Ok((ahead, behind))
228 }
229
230 pub async fn branch_info_parallel() -> Result<(String, Option<String>, u32, u32)> {
232 let (current_result, upstream_result) =
234 tokio::join!(Self::current_branch(), Self::upstream_branch());
235
236 let current = current_result?;
237
238 match upstream_result {
239 Ok(upstream) => {
240 let (ahead, behind) = Self::ahead_behind_counts().await.unwrap_or((0, 0));
242 Ok((current, Some(upstream), ahead, behind))
243 }
244 Err(_) => {
245 Ok((current, None, 0, 0))
247 }
248 }
249 }
250
251 pub async fn local_branches() -> Result<Vec<String>> {
253 let output = Self::run(&["branch", "--format=%(refname:short)"]).await?;
254 let branches: Vec<String> = output
255 .lines()
256 .map(|line| line.trim().to_string())
257 .filter(|branch| !branch.is_empty())
258 .collect();
259 Ok(branches)
260 }
261
262 pub async fn recent_branches(limit: Option<usize>) -> Result<Vec<String>> {
264 let (output, current_branch) = tokio::try_join!(
265 Self::run(&[
266 "for-each-ref",
267 "--sort=-committerdate",
268 "--format=%(refname:short)",
269 "refs/heads/",
270 ]),
271 Self::current_branch()
272 )?;
273
274 let mut branches: Vec<String> = output
275 .lines()
276 .map(|s| s.trim().to_string())
277 .filter(|branch| !branch.is_empty() && branch != ¤t_branch)
278 .collect();
279
280 if let Some(limit) = limit {
281 branches.truncate(limit);
282 }
283
284 Ok(branches)
285 }
286
287 pub async fn merged_branches() -> Result<Vec<String>> {
289 let output = Self::run(&["branch", "--merged"]).await?;
290 let branches: Vec<String> = output
291 .lines()
292 .map(|line| line.trim().trim_start_matches("* ").to_string())
293 .filter(|branch| !branch.is_empty())
294 .collect();
295 Ok(branches)
296 }
297
298 pub async fn is_working_directory_clean() -> Result<bool> {
300 let output = Self::run(&["status", "--porcelain"]).await?;
301 Ok(output.trim().is_empty())
302 }
303
304 pub async fn staged_files() -> Result<Vec<String>> {
306 let output = Self::run(&["diff", "--cached", "--name-only"]).await?;
307 let files: Vec<String> = output
308 .lines()
309 .map(|line| line.trim().to_string())
310 .filter(|file| !file.is_empty())
311 .collect();
312 Ok(files)
313 }
314
315 pub async fn get_recent_activity_timeline(limit: usize) -> Result<Vec<String>> {
317 let output = Self::run(&[
318 "log",
319 "--oneline",
320 "--decorate",
321 "--graph",
322 "--all",
323 &format!("--max-count={limit}"),
324 "--pretty=format:%C(auto)%h %s %C(dim)(%cr) %C(bold blue)<%an>%C(reset)",
325 ])
326 .await?;
327
328 let lines: Vec<String> = output.lines().map(|s| s.to_string()).collect();
329 Ok(lines)
330 }
331
332 pub async fn check_github_pr_status() -> Result<Option<String>> {
334 match tokio::process::Command::new("gh")
335 .args(["pr", "status", "--json", "currentBranch"])
336 .output()
337 .await
338 {
339 Ok(output) if output.status.success() => {
340 let stdout = String::from_utf8_lossy(&output.stdout);
341 if stdout.trim().is_empty() || stdout.contains("null") {
342 Ok(Some("❌ No open PR for current branch".to_string()))
343 } else {
344 Ok(Some("✅ Open PR found for current branch".to_string()))
345 }
346 }
347 _ => Ok(None), }
349 }
350
351 pub async fn get_branch_differences(current_branch: &str) -> Result<Vec<String>> {
353 let mut differences = Vec::new();
354
355 let checks = ["main", "master", "develop"]
357 .iter()
358 .filter(|&&branch| branch != current_branch)
359 .map(|&main_branch| async move {
360 if Self::run(&[
362 "rev-parse",
363 "--verify",
364 &format!("refs/heads/{main_branch}"),
365 ])
366 .await
367 .is_ok()
368 {
369 if let Ok(output) = Self::run(&[
371 "rev-list",
372 "--left-right",
373 "--count",
374 &format!("{main_branch}...{current_branch}"),
375 ])
376 .await
377 {
378 let parts: Vec<&str> = output.split_whitespace().collect();
379 if parts.len() == 2 {
380 let behind: u32 = parts[0].parse().unwrap_or(0);
381 let ahead: u32 = parts[1].parse().unwrap_or(0);
382
383 if ahead > 0 || behind > 0 {
384 let mut status_parts = Vec::new();
385 if ahead > 0 {
386 status_parts.push(format!("{ahead} ahead"));
387 }
388 if behind > 0 {
389 status_parts.push(format!("{behind} behind"));
390 }
391 return Some(format!(
392 "📊 vs {}: {}",
393 main_branch,
394 status_parts.join(", ")
395 ));
396 } else {
397 return Some(format!("✅ vs {main_branch}: Up to date"));
398 }
399 }
400 }
401 }
402 None
403 });
404
405 let results = futures::future::join_all(checks).await;
407 if let Some(diff) = results.into_iter().flatten().next() {
408 differences.push(diff);
409 }
410
411 Ok(differences)
412 }
413}
414
415pub struct BranchOperations;
417
418impl BranchOperations {
419 pub fn create(name: &str, from: Option<&str>) -> Result<()> {
421 let mut args = vec!["checkout", "-b", name];
422 if let Some(base) = from {
423 args.push(base);
424 }
425 GitOperations::run_status(&args)
426 }
427
428 pub fn delete(name: &str, force: bool) -> Result<()> {
430 let flag = if force { "-D" } else { "-d" };
431 GitOperations::run_status(&["branch", flag, name])
432 }
433
434 pub fn rename(new_name: &str) -> Result<()> {
436 GitOperations::run_status(&["branch", "-m", new_name])
437 }
438
439 pub fn switch(name: &str) -> Result<()> {
441 GitOperations::run_status(&["checkout", name])
442 }
443
444 pub fn exists(name: &str) -> Result<bool> {
446 match GitOperations::run(&["rev-parse", "--verify", &format!("refs/heads/{name}")]) {
447 Ok(_) => Ok(true),
448 Err(GitXError::GitCommand(_)) => Ok(false),
449 Err(e) => Err(e),
450 }
451 }
452}
453
454pub struct CommitOperations;
456
457impl CommitOperations {
458 pub fn fixup(commit_hash: &str) -> Result<()> {
460 GitOperations::run_status(&["commit", "--fixup", commit_hash])
461 }
462
463 pub fn undo_last() -> Result<()> {
465 GitOperations::run_status(&["reset", "--soft", "HEAD~1"])
466 }
467
468 pub fn get_message(commit_hash: &str) -> Result<String> {
470 GitOperations::run(&["log", "-1", "--pretty=format:%s", commit_hash])
471 }
472
473 pub fn get_author(commit_hash: &str) -> Result<String> {
475 GitOperations::run(&["log", "-1", "--pretty=format:%an <%ae>", commit_hash])
476 }
477}
478
479pub struct RemoteOperations;
481
482impl RemoteOperations {
483 pub fn set_upstream(remote: &str, branch: &str) -> Result<()> {
485 GitOperations::run_status(&["branch", "--set-upstream-to", &format!("{remote}/{branch}")])
486 }
487
488 pub fn push(remote: Option<&str>, branch: Option<&str>) -> Result<()> {
490 let mut args = vec!["push"];
491 if let Some(r) = remote {
492 args.push(r);
493 }
494 if let Some(b) = branch {
495 args.push(b);
496 }
497 GitOperations::run_status(&args)
498 }
499
500 pub fn fetch(remote: Option<&str>) -> Result<()> {
502 let mut args = vec!["fetch"];
503 if let Some(r) = remote {
504 args.push(r);
505 }
506 GitOperations::run_status(&args)
507 }
508
509 pub fn list() -> Result<Vec<String>> {
511 let output = GitOperations::run(&["remote"])?;
512 let remotes: Vec<String> = output
513 .lines()
514 .map(|line| line.trim().to_string())
515 .filter(|remote| !remote.is_empty())
516 .collect();
517 Ok(remotes)
518 }
519}