diff --git a/src/git.rs b/src/git.rs index 4051d6f..10b1344 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,173 +1,172 @@ use anyhow::{Context, Result}; -use std::path::Path; +use std::process::Command; +use std::string::ToString; #[derive(Debug, Clone)] -pub struct ChangedFile { - pub path: String, - pub status: FileStatus, - pub additions: usize, - pub deletions: usize, - pub is_new: bool, - pub is_deleted: bool, - pub is_renamed: bool, +pub struct GitCommit { pub old_path: Option, + pub new_path: Option, + pub sha: String, + pub message: String, + pub author: String, } -#[derive(Debug, Clone, PartialEq)] -pub enum FileStatus { - Added, - Deleted, - Modified, - Renamed, - Unknown, +pub fn get_recent_commits(limit: usize) -> Result> { + let output = Command::new("git") + .args(&[ + "log", + "--pretty=format:%H|%s|%an", + &format!("-n{}", limit), + ]) + .output() + .context("Failed to execute git log")?; + + if !output.status.success() { + return Err(anyhow::anyhow!("Git log command failed")); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let mut commits = Vec::new(); + + for line in stdout.lines() { + let parts: Vec<&str> = line.splitn(3, "|").collect(); + if parts.len() >= 3 { + commits.push(GitCommit { + old_path: None, + new_path: None, + sha: parts[0].to_string(), + message: parts[1].to_string(), + author: parts[2].to_string(), + }); + } + } + + Ok(commits) +} + +pub fn get_diff_from_sha(sha: &str) -> Result> { + let output = Command::new("git") + .args(&["show", "--stat", sha]) + .output() + .context("Failed to execute git show")?; + + if !output.status.success() { + return Err(anyhow::anyhow!("Git show command failed")); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let mut changes = Vec::new(); + + for line in stdout.lines() { + if line.contains("|") { + let parts: Vec<&str> = line.split("|").collect(); + if parts.len() == 2 { + let path_parts: Vec<&str> = parts[0].split(" -> ").collect(); + let (old_path, new_path) = if path_parts.len() == 2 { + (Some(path_parts[0].to_string()), Some(path_parts[1].to_string())) + } else { + (None, Some(parts[0].to_string())) + }; + + changes.push(Change { + old_path, + new_path, + additions: 0, + deletions: 0, + }); + } + } + } + + Ok(changes) } #[derive(Debug)] -pub struct StagedChanges { - pub files: Vec, - pub diff_text: String, +pub struct Change { + pub old_path: Option, + pub new_path: Option, + pub additions: usize, + pub deletions: usize, } -pub struct GitRepo { - repo_path: std::path::PathBuf, -} +pub fn get_staged_diff() -> Result> { + let output = Command::new("git") + .args(&["diff", "--cached", "--stat"]) + .output() + .context("Failed to execute git diff --cached")?; -impl GitRepo { - pub fn open(path: &Path) -> Result { - let repo = Self::find_repo_at(path)?; - Ok(Self { repo_path: repo }) + if !output.status.success() { + return Err(anyhow::anyhow!("Git diff command failed")); } - fn find_repo_at(path: &Path) -> Result { - let mut current = Some(path); - while let Some(p) = current { - let git_dir = p.join(".git"); - if git_dir.exists() && git_dir.is_dir() { - return Ok(p.to_path_buf()); - } - current = p.parent(); - } - anyhow::bail!("Not in a git repository") - } + let stdout = String::from_utf8_lossy(&output.stdout); + let mut changes = Vec::new(); - pub fn find_repo() -> Result { - let current_dir = std::env::current_dir().context("Failed to get current directory")?; - Self::open(¤t_dir) - } - - pub fn get_staged_changes(&self) -> Result { - let diff_text = self.run_git(&["diff", "--staged", "--no-color"])?; - let status_output = self.run_git(&["status", "--porcelain"])?; - - let files = self.parse_status(&status_output)?; - let diff_text = diff_text; - - Ok(StagedChanges { files, diff_text }) - } - - fn parse_status(&self, output: &str) -> Result> { - let mut files = Vec::new(); - - for line in output.lines() { - let line = line.trim(); - if line.is_empty() { - continue; - } - - let status_char = line.chars().next().unwrap_or(' '); - let xy = line.get(..2).unwrap_or(" "); - let (status, is_new, is_deleted, is_renamed) = - match (xy.chars().next(), xy.chars().nth(1)) { - (Some('A'), _) => (FileStatus::Added, true, false, false), - (Some('D'), _) => (FileStatus::Deleted, false, true, false), - (Some('R'), _) => (FileStatus::Renamed, false, false, true), - (Some('M'), _) | (Some(_), Some('M')) => { - (FileStatus::Modified, false, false, false) - } - (Some('?'), _) => { - files.push(ChangedFile { - path: line[3..].trim().to_string(), - status: FileStatus::Added, - additions: 0, - deletions: 0, - is_new: true, - is_deleted: false, - is_renamed: false, - old_path: None, - }); - continue; - } - _ => (FileStatus::Unknown, false, false, false), - }; - - let path_parts: Vec<&str> = line[3..].splitn(2, " -> ").collect(); - let path = path_parts[1].to_string(); - let old_path = if is_renamed && path_parts.len() > 1 { - Some(path_parts[0].to_string()) + for line in stdout.lines() { + let parts: Vec<&str> = line.split("|").collect(); + if parts.len() == 2 { + let path_parts: Vec<&str> = parts[0].split(" -> ").collect(); + let (old_path, new_path) = if path_parts.len() == 2 { + (Some(path_parts[0].to_string()), Some(path_parts[1].to_string())) } else { - None + (None, Some(parts[0].to_string())) }; - files.push(ChangedFile { - path, - status, - additions: 0, - deletions: 0, - is_new, - is_deleted, - is_renamed, + let stat_parts: Vec<&str> = parts[1].trim().split(" ").collect(); + let mut additions = 0; + let mut deletions = 0; + + for stat in stat_parts { + if stat.contains('+') && stat.parse::().is_ok() { + additions = stat.replace('+', "").parse().unwrap_or(0); + } + if stat.contains('-') && stat.parse::().is_ok() { + deletions = stat.replace('-', "").parse().unwrap_or(0); + } + } + + changes.push(Change { old_path, + new_path, + additions, + deletions, }); } - - Ok(files) } - fn run_git(&self, args: &[&str]) -> Result { - let mut cmd = std::process::Command::new("git"); - cmd.current_dir(&self.repo_path); - cmd.args(args); - - let output = cmd.output().context("Failed to execute git command")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - anyhow::bail!("Git command failed: {}", stderr); - } - - Ok(String::from_utf8_lossy(&output.stdout).to_string()) - } - - pub fn create_commit(&self, message: &str, dry_run: bool) -> Result<()> { - if dry_run { - println!("[DRY-RUN] Would create commit with message:\n{}", message); - return Ok(()); - } - - let mut cmd = std::process::Command::new("git"); - cmd.current_dir(&self.repo_path); - cmd.args(&["commit", "-m", message]); - - let output = cmd.output().context("Failed to execute git commit")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - anyhow::bail!("Git commit failed: {}", stderr); - } - - println!("Successfully created commit"); - Ok(()) - } - - pub fn get_head_commit(&self) -> Result { - let output = self.run_git(&["rev-parse", "HEAD"])?; - Ok(output.trim().to_string()) - } - - pub fn has_config(&self) -> Result { - match self.run_git(&["config", "--list"]) { - Ok(_) => Ok(true), - Err(_) => Ok(false), - } - } + Ok(changes) +} + +pub fn get_diff_stats() -> Result<(usize, usize)> { + let output = Command::new("git") + .args(&["diff", "--shortstat"]) + .output() + .context("Failed to execute git diff")?; + + if !output.status.success() { + return Err(anyhow::anyhow!("Git diff command failed")); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let mut insertions = 0; + let mut deletions = 0; + + for line in stdout.lines() { + let parts: Vec<&str> = line.split(",").collect(); + for part in parts { + let part = part.trim(); + if let Some(insertions_str) = part.strip_suffix(" insertions(+)") { + if let Ok(count) = insertions_str.trim().parse::() { + insertions = count; + } + } + if let Some(deletions_str) = part.strip_suffix(" deletions(-)") { + if let Ok(count) = deletions_str.trim().parse::() { + deletions = count; + } + } + } + } + + Ok((insertions, deletions)) }