git_x/core/
git.rs

1use crate::{GitXError, Result};
2use std::process::Command;
3
4/// Core git operations abstraction
5pub struct GitOperations;
6
7impl GitOperations {
8    /// Execute a git command and return stdout as String
9    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    /// Execute a git command and return success status
22    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    /// Get current branch name
36    pub fn current_branch() -> Result<String> {
37        Self::run(&["rev-parse", "--abbrev-ref", "HEAD"])
38    }
39
40    /// Get repository root path
41    pub fn repo_root() -> Result<String> {
42        Self::run(&["rev-parse", "--show-toplevel"])
43    }
44
45    /// Check if a commit exists
46    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    /// Get short commit hash
55    pub fn short_hash(commit: &str) -> Result<String> {
56        Self::run(&["rev-parse", "--short", commit])
57    }
58
59    /// Get upstream branch for current branch
60    pub fn upstream_branch() -> Result<String> {
61        Self::run(&["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"])
62    }
63
64    /// Get ahead/behind counts compared to upstream
65    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    /// Get branch information in an optimized way to reduce git calls
74    pub fn branch_info_optimized() -> Result<(String, Option<String>, u32, u32)> {
75        // Get current branch
76        let current = Self::current_branch()?;
77
78        // Try to get upstream - if this fails, there's no upstream
79        match Self::upstream_branch() {
80            Ok(upstream) => {
81                // Only check ahead/behind if upstream exists
82                let (ahead, behind) = Self::ahead_behind_counts().unwrap_or((0, 0));
83                Ok((current, Some(upstream), ahead, behind))
84            }
85            Err(_) => {
86                // No upstream configured
87                Ok((current, None, 0, 0))
88            }
89        }
90    }
91
92    /// Get all local branches
93    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    /// Get recent branches sorted by commit date
104    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 != &current_branch)
117            .collect();
118
119        if let Some(limit) = limit {
120            branches.truncate(limit);
121        }
122
123        Ok(branches)
124    }
125
126    /// Get merged branches
127    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    /// Check if working directory is clean
138    pub fn is_working_directory_clean() -> Result<bool> {
139        let output = Self::run(&["status", "--porcelain"])?;
140        Ok(output.trim().is_empty())
141    }
142
143    /// Get staged files
144    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
155/// Async Git operations for parallel execution
156pub struct AsyncGitOperations;
157
158impl AsyncGitOperations {
159    /// Execute a git command asynchronously and return stdout as String
160    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    /// Execute a git command asynchronously and return success status
176    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    /// Get current branch name
193    pub async fn current_branch() -> Result<String> {
194        Self::run(&["rev-parse", "--abbrev-ref", "HEAD"]).await
195    }
196
197    /// Get repository root path
198    pub async fn repo_root() -> Result<String> {
199        Self::run(&["rev-parse", "--show-toplevel"]).await
200    }
201
202    /// Check if a commit exists
203    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    /// Get short commit hash
212    pub async fn short_hash(commit: &str) -> Result<String> {
213        Self::run(&["rev-parse", "--short", commit]).await
214    }
215
216    /// Get upstream branch for current branch
217    pub async fn upstream_branch() -> Result<String> {
218        Self::run(&["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]).await
219    }
220
221    /// Get ahead/behind counts compared to upstream
222    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    /// Get branch information optimized for parallel execution
231    pub async fn branch_info_parallel() -> Result<(String, Option<String>, u32, u32)> {
232        // Run current branch and upstream branch queries in parallel
233        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                // Only check ahead/behind if upstream exists
241                let (ahead, behind) = Self::ahead_behind_counts().await.unwrap_or((0, 0));
242                Ok((current, Some(upstream), ahead, behind))
243            }
244            Err(_) => {
245                // No upstream configured
246                Ok((current, None, 0, 0))
247            }
248        }
249    }
250
251    /// Get all local branches
252    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    /// Get recent branches sorted by commit date
263    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 != &current_branch)
278            .collect();
279
280        if let Some(limit) = limit {
281            branches.truncate(limit);
282        }
283
284        Ok(branches)
285    }
286
287    /// Get merged branches
288    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    /// Check if working directory is clean
299    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    /// Get staged files
305    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    /// Get recent activity timeline
316    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    /// Check GitHub PR status using external gh command
333    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), // GitHub CLI not available or error
348        }
349    }
350
351    /// Get branch differences against main branches
352    pub async fn get_branch_differences(current_branch: &str) -> Result<Vec<String>> {
353        let mut differences = Vec::new();
354
355        // Check against main/master/develop in parallel
356        let checks = ["main", "master", "develop"]
357            .iter()
358            .filter(|&&branch| branch != current_branch)
359            .map(|&main_branch| async move {
360                // Check if this main branch exists
361                if Self::run(&[
362                    "rev-parse",
363                    "--verify",
364                    &format!("refs/heads/{main_branch}"),
365                ])
366                .await
367                .is_ok()
368                {
369                    // Get ahead/behind count
370                    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        // Execute all branch checks in parallel
406        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
415/// Branch operations
416pub struct BranchOperations;
417
418impl BranchOperations {
419    /// Create a new branch
420    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    /// Delete a branch
429    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    /// Rename current branch
435    pub fn rename(new_name: &str) -> Result<()> {
436        GitOperations::run_status(&["branch", "-m", new_name])
437    }
438
439    /// Switch to a branch
440    pub fn switch(name: &str) -> Result<()> {
441        GitOperations::run_status(&["checkout", name])
442    }
443
444    /// Check if branch exists
445    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
454/// Commit operations
455pub struct CommitOperations;
456
457impl CommitOperations {
458    /// Create a fixup commit
459    pub fn fixup(commit_hash: &str) -> Result<()> {
460        GitOperations::run_status(&["commit", "--fixup", commit_hash])
461    }
462
463    /// Undo last commit (soft reset)
464    pub fn undo_last() -> Result<()> {
465        GitOperations::run_status(&["reset", "--soft", "HEAD~1"])
466    }
467
468    /// Get commit message
469    pub fn get_message(commit_hash: &str) -> Result<String> {
470        GitOperations::run(&["log", "-1", "--pretty=format:%s", commit_hash])
471    }
472
473    /// Get commit author
474    pub fn get_author(commit_hash: &str) -> Result<String> {
475        GitOperations::run(&["log", "-1", "--pretty=format:%an <%ae>", commit_hash])
476    }
477}
478
479/// Remote operations
480pub struct RemoteOperations;
481
482impl RemoteOperations {
483    /// Set upstream for current branch
484    pub fn set_upstream(remote: &str, branch: &str) -> Result<()> {
485        GitOperations::run_status(&["branch", "--set-upstream-to", &format!("{remote}/{branch}")])
486    }
487
488    /// Push to remote
489    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    /// Fetch from remote
501    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    /// Get remotes
510    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}