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/// Branch operations
156pub struct BranchOperations;
157
158impl BranchOperations {
159    /// Create a new branch
160    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    /// Delete a branch
169    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    /// Rename current branch
175    pub fn rename(new_name: &str) -> Result<()> {
176        GitOperations::run_status(&["branch", "-m", new_name])
177    }
178
179    /// Switch to a branch
180    pub fn switch(name: &str) -> Result<()> {
181        GitOperations::run_status(&["checkout", name])
182    }
183
184    /// Check if branch exists
185    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
194/// Commit operations
195pub struct CommitOperations;
196
197impl CommitOperations {
198    /// Create a fixup commit
199    pub fn fixup(commit_hash: &str) -> Result<()> {
200        GitOperations::run_status(&["commit", "--fixup", commit_hash])
201    }
202
203    /// Undo last commit (soft reset)
204    pub fn undo_last() -> Result<()> {
205        GitOperations::run_status(&["reset", "--soft", "HEAD~1"])
206    }
207
208    /// Get commit message
209    pub fn get_message(commit_hash: &str) -> Result<String> {
210        GitOperations::run(&["log", "-1", "--pretty=format:%s", commit_hash])
211    }
212
213    /// Get commit author
214    pub fn get_author(commit_hash: &str) -> Result<String> {
215        GitOperations::run(&["log", "-1", "--pretty=format:%an <%ae>", commit_hash])
216    }
217}
218
219/// Remote operations
220pub struct RemoteOperations;
221
222impl RemoteOperations {
223    /// Set upstream for current branch
224    pub fn set_upstream(remote: &str, branch: &str) -> Result<()> {
225        GitOperations::run_status(&["branch", "--set-upstream-to", &format!("{remote}/{branch}")])
226    }
227
228    /// Push to remote
229    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    /// Fetch from remote
241    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    /// Get remotes
250    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}