diff --git a/src/git.rs b/src/git.rs index b10bca9..4051d6f 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,30 +1,19 @@ use anyhow::{Context, Result}; -use std::process::Command; - -#[derive(Debug, Clone)] -pub struct GitRepo { - pub repo_path: String, -} - -#[derive(Debug, Clone)] -pub struct StagedChanges { - pub files: Vec, - pub diff_text: String, -} +use std::path::Path; #[derive(Debug, Clone)] pub struct ChangedFile { pub path: String, - pub old_path: Option, pub status: FileStatus, pub additions: usize, pub deletions: usize, pub is_new: bool, pub is_deleted: bool, pub is_renamed: bool, + pub old_path: Option, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum FileStatus { Added, Deleted, @@ -33,123 +22,152 @@ pub enum FileStatus { Unknown, } +#[derive(Debug)] +pub struct StagedChanges { + pub files: Vec, + pub diff_text: String, +} + +pub struct GitRepo { + repo_path: std::path::PathBuf, +} + 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")?; + 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!("Not in a git repository")); + 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 repo_path = String::from_utf8_lossy(&output.stdout) - .trim() - .to_string(); - - Ok(GitRepo { repo_path }) + 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 output = Command::new("git") - .args(&["diff", "--cached", "--stat"]) - .output() - .context("Failed to execute git diff --cached")?; + let diff_text = self.run_git(&["diff", "--staged", "--no-color"])?; + let status_output = self.run_git(&["status", "--porcelain"])?; - if !output.status.success() { - return Err(anyhow::anyhow!("Git diff command failed")); - } + let files = self.parse_status(&status_output)?; + let diff_text = diff_text; - let stdout = String::from_utf8_lossy(&output.stdout); - let mut changes = Vec::new(); + Ok(StagedChanges { files, diff_text }) + } - for line in stdout.lines() { - if line.is_empty() || !line.contains('|') { + 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 parts: Vec<&str> = line.split('|').collect(); - if parts.len() != 2 { - 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[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 { - (FileStatus::Unknown, 0, 0) - }; - - let is_new = matches!(status, FileStatus::Added); - let is_deleted = matches!(status, FileStatus::Deleted); - let is_renamed = matches!(status, FileStatus::Renamed); - - let old_path = if is_renamed { - Some(path.split("=>").next().unwrap_or("").trim().to_string()) + 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()) } else { None }; - changes.push(ChangedFile { - path: if is_renamed { - path.split("=>").nth(1).unwrap_or("").trim().to_string() - } else { - path - }, - old_path, + files.push(ChangedFile { + path, status, - additions, - deletions, + additions: 0, + deletions: 0, is_new, is_deleted, is_renamed, + old_path, }); } - 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(files) } - pub fn create_commit(&self, message: &str, _dry_run: bool) -> Result<()> { - let output = Command::new("git") - .args(&["commit", "-m", message]) - .output() - .context("Failed to execute git commit")?; + 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); - return Err(anyhow::anyhow!("Git commit failed: {}", 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 has_config(&self) -> bool { - true + 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), + } } }