diff --git a/src/git.rs b/src/git.rs index 10b1344..03c9d48 100644 --- a/src/git.rs +++ b/src/git.rs @@ -3,170 +3,159 @@ use std::process::Command; use std::string::ToString; #[derive(Debug, Clone)] -pub struct GitCommit { +pub struct GitRepo { + repo_path: String, +} + +#[derive(Debug, Clone)] +pub struct StagedChanges { + pub files: Vec, + pub diff_text: String, +} + +#[derive(Debug, Clone)] +pub struct ChangedFile { + pub path: String, pub old_path: Option, - pub new_path: Option, - pub sha: String, - pub message: String, - pub author: String, -} - -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 Change { - pub old_path: Option, - pub new_path: Option, + pub status: FileStatus, pub additions: usize, pub deletions: usize, + pub is_new: bool, + pub is_deleted: bool, + pub is_renamed: bool, } -pub fn get_staged_diff() -> Result> { - let output = Command::new("git") - .args(&["diff", "--cached", "--stat"]) - .output() - .context("Failed to execute git diff --cached")?; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileStatus { + Added, + Deleted, + Modified, + Renamed, + Unknown, +} - if !output.status.success() { - return Err(anyhow::anyhow!("Git diff command failed")); +impl GitRepo { + pub fn find_repo() -> Result { + let output = Command::new("git") + .args(&["rev-parse", "--show-toplevel"]) + .output() + .context("Failed to execute git rev-parse")?; + + if !output.status.success() { + return Err(anyhow::anyhow!("Not in a git repository")); + } + + let repo_path = String::from_utf8_lossy(&output.stdout) + .trim() + .to_string(); + + Ok(GitRepo { repo_path }) } - let stdout = String::from_utf8_lossy(&output.stdout); - let mut changes = Vec::new(); + pub fn get_staged_changes(&self) -> Result { + let output = Command::new("git") + .args(&["diff", "--cached", "--stat"]) + .output() + .context("Failed to execute git diff --cached")?; - 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())) + if !output.status.success() { + return Err(anyhow::anyhow!("Git diff command failed")); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let mut changes = Vec::new(); + + for line in stdout.lines() { + if line.is_empty() || !line.contains('|') { + continue; + } + + let parts: Vec<&str> = line.split('|').collect(); + if parts.len() != 2 { + continue; + } + + let path = parts[0].trim().to_string(); + let stat_part = parts[1].trim().to_string(); + + let (status, additions, deletions) = if stat_part.contains("+") && stat_part.contains("-") { + let add_parts: Vec<&str> = stat_part.split('+').collect(); + let del_parts: Vec<&str> = add_parts[1].split('-').collect(); + let adds: usize = add_parts[0].trim().parse().unwrap_or(0); + let dels: usize = del_parts[0].trim().parse().unwrap_or(0); + (FileStatus::Modified, adds, dels) + } else if stat_part.contains("+") { + let adds: usize = stat_part.replace("+", "").trim().parse().unwrap_or(0); + (FileStatus::Added, adds, 0) + } else if stat_part.contains("-") { + let dels: usize = stat_part.replace("-", "").trim().parse().unwrap_or(0); + (FileStatus::Deleted, 0, dels) + } else if stat_part.contains("=>") { + (FileStatus::Renamed, 0, 0) } else { - (None, Some(parts[0].to_string())) + (FileStatus::Unknown, 0, 0) }; - let stat_parts: Vec<&str> = parts[1].trim().split(" ").collect(); - let mut additions = 0; - let mut deletions = 0; + let is_new = matches!(status, FileStatus::Added); + let is_deleted = matches!(status, FileStatus::Deleted); + let is_renamed = matches!(status, FileStatus::Renamed); - 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); - } - } + let old_path = if is_renamed { + Some(path.split("=>").next().unwrap_or("").trim().to_string()) + } else { + None + }; - changes.push(Change { + changes.push(ChangedFile { + path: if is_renamed { + path.split("=>").nth(1).unwrap_or("").trim().to_string() + } else { + path + }, old_path, - new_path, + status, additions, deletions, + is_new, + is_deleted, + is_renamed, }); } + + let diff_output = Command::new("git") + .args(&["diff", "--cached"]) + .output() + .context("Failed to execute git diff --cached")?; + + let diff_text = if diff_output.status.success() { + String::from_utf8_lossy(&diff_output.stdout).to_string() + } else { + String::new() + }; + + Ok(StagedChanges { files: changes, diff_text }) } - 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; - } - } + pub fn create_commit(&self, message: &str, dry_run: bool) -> Result<()> { + if dry_run { + println!("[DRY RUN] Would commit with message: {}", message); + return Ok(()); } + + let output = Command::new("git") + .args(&["commit", "-m", message]) + .output() + .context("Failed to execute git commit")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow::anyhow!("Git commit failed: {}", stderr)); + } + + Ok(()) } - Ok((insertions, deletions)) + pub fn has_config(&self) -> bool { + true + } }