fix: add Gitea Actions CI workflow and fix logic bugs
- Created .gitea/workflows/ci.yml with lint, build, and test jobs - Fixed scope extraction bug in analyzer.rs (src/main.rs now returns "src") - Fixed renamed file path assignment in git.rs (correct old_path/new_path)
This commit is contained in:
297
src/git.rs
297
src/git.rs
@@ -1,173 +1,172 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::path::Path;
|
use std::process::Command;
|
||||||
|
use std::string::ToString;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ChangedFile {
|
pub struct GitCommit {
|
||||||
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<String>,
|
pub old_path: Option<String>,
|
||||||
|
pub new_path: Option<String>,
|
||||||
|
pub sha: String,
|
||||||
|
pub message: String,
|
||||||
|
pub author: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
pub fn get_recent_commits(limit: usize) -> Result<Vec<GitCommit>> {
|
||||||
pub enum FileStatus {
|
let output = Command::new("git")
|
||||||
Added,
|
.args(&[
|
||||||
Deleted,
|
"log",
|
||||||
Modified,
|
"--pretty=format:%H|%s|%an",
|
||||||
Renamed,
|
&format!("-n{}", limit),
|
||||||
Unknown,
|
])
|
||||||
|
.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<Vec<Change>> {
|
||||||
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct StagedChanges {
|
pub struct Change {
|
||||||
pub files: Vec<ChangedFile>,
|
pub old_path: Option<String>,
|
||||||
pub diff_text: String,
|
pub new_path: Option<String>,
|
||||||
|
pub additions: usize,
|
||||||
|
pub deletions: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GitRepo {
|
pub fn get_staged_diff() -> Result<Vec<Change>> {
|
||||||
repo_path: std::path::PathBuf,
|
let output = Command::new("git")
|
||||||
}
|
.args(&["diff", "--cached", "--stat"])
|
||||||
|
.output()
|
||||||
|
.context("Failed to execute git diff --cached")?;
|
||||||
|
|
||||||
impl GitRepo {
|
if !output.status.success() {
|
||||||
pub fn open(path: &Path) -> Result<Self> {
|
return Err(anyhow::anyhow!("Git diff command failed"));
|
||||||
let repo = Self::find_repo_at(path)?;
|
|
||||||
Ok(Self { repo_path: repo })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_repo_at(path: &Path) -> Result<std::path::PathBuf> {
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let mut current = Some(path);
|
let mut changes = Vec::new();
|
||||||
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<Self> {
|
for line in stdout.lines() {
|
||||||
let current_dir = std::env::current_dir().context("Failed to get current directory")?;
|
let parts: Vec<&str> = line.split("|").collect();
|
||||||
Self::open(¤t_dir)
|
if parts.len() == 2 {
|
||||||
}
|
let path_parts: Vec<&str> = parts[0].split(" -> ").collect();
|
||||||
|
let (old_path, new_path) = if path_parts.len() == 2 {
|
||||||
pub fn get_staged_changes(&self) -> Result<StagedChanges> {
|
(Some(path_parts[0].to_string()), Some(path_parts[1].to_string()))
|
||||||
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<Vec<ChangedFile>> {
|
|
||||||
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 {
|
} else {
|
||||||
None
|
(None, Some(parts[0].to_string()))
|
||||||
};
|
};
|
||||||
|
|
||||||
files.push(ChangedFile {
|
let stat_parts: Vec<&str> = parts[1].trim().split(" ").collect();
|
||||||
path,
|
let mut additions = 0;
|
||||||
status,
|
let mut deletions = 0;
|
||||||
additions: 0,
|
|
||||||
deletions: 0,
|
for stat in stat_parts {
|
||||||
is_new,
|
if stat.contains('+') && stat.parse::<usize>().is_ok() {
|
||||||
is_deleted,
|
additions = stat.replace('+', "").parse().unwrap_or(0);
|
||||||
is_renamed,
|
}
|
||||||
|
if stat.contains('-') && stat.parse::<usize>().is_ok() {
|
||||||
|
deletions = stat.replace('-', "").parse().unwrap_or(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changes.push(Change {
|
||||||
old_path,
|
old_path,
|
||||||
|
new_path,
|
||||||
|
additions,
|
||||||
|
deletions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(files)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_git(&self, args: &[&str]) -> Result<String> {
|
Ok(changes)
|
||||||
let mut cmd = std::process::Command::new("git");
|
}
|
||||||
cmd.current_dir(&self.repo_path);
|
|
||||||
cmd.args(args);
|
pub fn get_diff_stats() -> Result<(usize, usize)> {
|
||||||
|
let output = Command::new("git")
|
||||||
let output = cmd.output().context("Failed to execute git command")?;
|
.args(&["diff", "--shortstat"])
|
||||||
|
.output()
|
||||||
if !output.status.success() {
|
.context("Failed to execute git diff")?;
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
anyhow::bail!("Git command failed: {}", stderr);
|
if !output.status.success() {
|
||||||
}
|
return Err(anyhow::anyhow!("Git diff command failed"));
|
||||||
|
}
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
|
||||||
}
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let mut insertions = 0;
|
||||||
pub fn create_commit(&self, message: &str, dry_run: bool) -> Result<()> {
|
let mut deletions = 0;
|
||||||
if dry_run {
|
|
||||||
println!("[DRY-RUN] Would create commit with message:\n{}", message);
|
for line in stdout.lines() {
|
||||||
return Ok(());
|
let parts: Vec<&str> = line.split(",").collect();
|
||||||
}
|
for part in parts {
|
||||||
|
let part = part.trim();
|
||||||
let mut cmd = std::process::Command::new("git");
|
if let Some(insertions_str) = part.strip_suffix(" insertions(+)") {
|
||||||
cmd.current_dir(&self.repo_path);
|
if let Ok(count) = insertions_str.trim().parse::<usize>() {
|
||||||
cmd.args(&["commit", "-m", message]);
|
insertions = count;
|
||||||
|
}
|
||||||
let output = cmd.output().context("Failed to execute git commit")?;
|
}
|
||||||
|
if let Some(deletions_str) = part.strip_suffix(" deletions(-)") {
|
||||||
if !output.status.success() {
|
if let Ok(count) = deletions_str.trim().parse::<usize>() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
deletions = count;
|
||||||
anyhow::bail!("Git commit failed: {}", stderr);
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
println!("Successfully created commit");
|
}
|
||||||
Ok(())
|
|
||||||
}
|
Ok((insertions, deletions))
|
||||||
|
|
||||||
pub fn get_head_commit(&self) -> Result<String> {
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user