From d3f7b36f7ea6f1e3971cc82c71607ceb38ca92f8 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Sun, 1 Feb 2026 12:20:54 +0000 Subject: [PATCH] Initial upload: Auto Commit Message Generator with CI/CD workflow --- src/git.rs | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 src/git.rs diff --git a/src/git.rs b/src/git.rs new file mode 100644 index 0000000..4051d6f --- /dev/null +++ b/src/git.rs @@ -0,0 +1,173 @@ +use anyhow::{Context, Result}; +use std::path::Path; + +#[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 old_path: Option, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum FileStatus { + Added, + Deleted, + Modified, + Renamed, + 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 open(path: &Path) -> Result { + let repo = Self::find_repo_at(path)?; + Ok(Self { repo_path: repo }) + } + + 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") + } + + 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()) + } else { + None + }; + + files.push(ChangedFile { + path, + status, + additions: 0, + deletions: 0, + is_new, + is_deleted, + is_renamed, + old_path, + }); + } + + 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), + } + } +}