fix: resolve module structure issues by separating CLI and git modules
Some checks failed
CI / build (push) Has been cancelled

- Removed duplicated git module code from cli.rs
- Created dedicated git.rs module with GitRepo, StagedChanges, ChangedFile, FileStatus definitions
- Updated all modules (analyzer, generator, prompt) to import from super::git
- Fixed module organization to resolve compilation errors
This commit is contained in:
2026-02-01 12:48:40 +00:00
parent 3b7844976f
commit 8a15c29a18

View File

@@ -1,30 +1,19 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::process::Command; use std::path::Path;
#[derive(Debug, Clone)]
pub struct GitRepo {
pub repo_path: String,
}
#[derive(Debug, Clone)]
pub struct StagedChanges {
pub files: Vec<ChangedFile>,
pub diff_text: String,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ChangedFile { pub struct ChangedFile {
pub path: String, pub path: String,
pub old_path: Option<String>,
pub status: FileStatus, pub status: FileStatus,
pub additions: usize, pub additions: usize,
pub deletions: usize, pub deletions: usize,
pub is_new: bool, pub is_new: bool,
pub is_deleted: bool, pub is_deleted: bool,
pub is_renamed: bool, pub is_renamed: bool,
pub old_path: Option<String>,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq)]
pub enum FileStatus { pub enum FileStatus {
Added, Added,
Deleted, Deleted,
@@ -33,123 +22,152 @@ pub enum FileStatus {
Unknown, Unknown,
} }
impl GitRepo { #[derive(Debug)]
pub fn find_repo() -> Result<Self> { pub struct StagedChanges {
let output = Command::new("git") pub files: Vec<ChangedFile>,
.args(&["rev-parse", "--show-toplevel"]) pub diff_text: String,
.output() }
.context("Failed to execute git rev-parse")?;
if !output.status.success() { pub struct GitRepo {
return Err(anyhow::anyhow!("Not in a git repository")); repo_path: std::path::PathBuf,
}
impl GitRepo {
pub fn open(path: &Path) -> Result<Self> {
let repo = Self::find_repo_at(path)?;
Ok(Self { repo_path: repo })
} }
let repo_path = String::from_utf8_lossy(&output.stdout) fn find_repo_at(path: &Path) -> Result<std::path::PathBuf> {
.trim() let mut current = Some(path);
.to_string(); 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")
}
Ok(GitRepo { repo_path }) pub fn find_repo() -> Result<Self> {
let current_dir = std::env::current_dir().context("Failed to get current directory")?;
Self::open(&current_dir)
} }
pub fn get_staged_changes(&self) -> Result<StagedChanges> { pub fn get_staged_changes(&self) -> Result<StagedChanges> {
let output = Command::new("git") let diff_text = self.run_git(&["diff", "--staged", "--no-color"])?;
.args(&["diff", "--cached", "--stat"]) let status_output = self.run_git(&["status", "--porcelain"])?;
.output()
.context("Failed to execute git diff --cached")?;
if !output.status.success() { let files = self.parse_status(&status_output)?;
return Err(anyhow::anyhow!("Git diff command failed")); let diff_text = diff_text;
Ok(StagedChanges { files, diff_text })
} }
let stdout = String::from_utf8_lossy(&output.stdout); fn parse_status(&self, output: &str) -> Result<Vec<ChangedFile>> {
let mut changes = Vec::new(); let mut files = Vec::new();
for line in stdout.lines() { for line in output.lines() {
if line.is_empty() || !line.contains('|') { let line = line.trim();
if line.is_empty() {
continue; continue;
} }
let parts: Vec<&str> = line.split('|').collect(); let status_char = line.chars().next().unwrap_or(' ');
if parts.len() != 2 { 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; 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 path_parts: Vec<&str> = line[3..].splitn(2, " -> ").collect();
let is_deleted = matches!(status, FileStatus::Deleted); let path = path_parts[1].to_string();
let is_renamed = matches!(status, FileStatus::Renamed); let old_path = if is_renamed && path_parts.len() > 1 {
Some(path_parts[0].to_string())
let old_path = if is_renamed {
Some(path.split("=>").next().unwrap_or("").trim().to_string())
} else { } else {
None None
}; };
changes.push(ChangedFile { files.push(ChangedFile {
path: if is_renamed { path,
path.split("=>").nth(1).unwrap_or("").trim().to_string()
} else {
path
},
old_path,
status, status,
additions, additions: 0,
deletions, deletions: 0,
is_new, is_new,
is_deleted, is_deleted,
is_renamed, is_renamed,
old_path,
}); });
} }
let diff_output = Command::new("git") Ok(files)
.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 })
} }
pub fn create_commit(&self, message: &str, _dry_run: bool) -> Result<()> { fn run_git(&self, args: &[&str]) -> Result<String> {
let output = Command::new("git") let mut cmd = std::process::Command::new("git");
.args(&["commit", "-m", message]) cmd.current_dir(&self.repo_path);
.output() cmd.args(args);
.context("Failed to execute git commit")?;
let output = cmd.output().context("Failed to execute git command")?;
if !output.status.success() { if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr); 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(()) Ok(())
} }
pub fn has_config(&self) -> bool { pub fn get_head_commit(&self) -> Result<String> {
true let output = self.run_git(&["rev-parse", "HEAD"])?;
Ok(output.trim().to_string())
}
pub fn has_config(&self) -> Result<bool> {
match self.run_git(&["config", "--list"]) {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
} }
} }