#![allow(dead_code)]
use super::{GitInfo, CommitInfo};
use anyhow::Result;
use chrono::{Utc, TimeZone};
use git2::{Repository, Sort};
use std::path::Path;
use tracing::{debug, info};
pub struct GitAnalyzer;
impl GitAnalyzer {
pub fn new() -> Self {
Self
}
pub async fn analyze(&self, path: &Path) -> Result<GitInfo> {
let repo = match Repository::open(path) {
Ok(repo) => repo,
Err(_) => {
return Ok(GitInfo {
is_git_repo: false,
..Default::default()
});
}
};
info!("Analyzing git repository at: {}", path.display());
let mut git_info = GitInfo {
is_git_repo: true,
..Default::default()
};
git_info.current_branch = self.get_current_branch(&repo);
git_info.remote_url = self.get_remote_url(&repo);
git_info.recent_commits = self.get_recent_commits(&repo, 50)?;
git_info.total_commits = self.get_total_commits(&repo)?;
git_info.contributors = self.get_contributors(&repo)?;
debug!("Git analysis complete: {} commits, {} contributors",
git_info.total_commits,
git_info.contributors.len());
Ok(git_info)
}
fn get_current_branch(&self, repo: &Repository) -> Option<String> {
match repo.head() {
Ok(head) => {
if let Some(name) = head.shorthand() {
Some(name.to_string())
} else {
None
}
}
Err(_) => None,
}
}
fn get_remote_url(&self, repo: &Repository) -> Option<String> {
match repo.find_remote("origin") {
Ok(remote) => {
remote.url().map(|s| s.to_string())
}
Err(_) => None,
}
}
fn get_recent_commits(&self, repo: &Repository, limit: usize) -> Result<Vec<CommitInfo>> {
let mut revwalk = repo.revwalk()?;
revwalk.set_sorting(Sort::TIME)?;
revwalk.push_head()?;
let mut commits = Vec::new();
for (i, oid) in revwalk.enumerate() {
if i >= limit {
break;
}
let oid = oid?;
let commit = repo.find_commit(oid)?;
let author = commit.author();
let committer = commit.committer();
let message = commit.message()
.unwrap_or("No message")
.to_string();
let author_name = author.name()
.unwrap_or("Unknown")
.to_string();
let time = committer.when();
let timestamp = time.seconds();
let date = Utc.timestamp_opt(timestamp, 0)
.single()
.unwrap_or_else(Utc::now);
let files_changed = self.get_changed_files(repo, &commit)?;
commits.push(CommitInfo {
hash: oid.to_string(),
message,
author: author_name,
date,
files_changed,
});
}
Ok(commits)
}
fn get_changed_files(&self, repo: &Repository, commit: &git2::Commit) -> Result<Vec<String>> {
let mut files = Vec::new();
let tree = commit.tree()?;
let parent_tree = if commit.parent_count() > 0 {
let parent = commit.parent(0)?;
Some(parent.tree()?)
} else {
None
};
let diff = if let Some(parent) = parent_tree {
repo.diff_tree_to_tree(Some(&parent), Some(&tree), None)?
} else {
repo.diff_tree_to_tree(None, Some(&tree), None)?
};
diff.foreach(
&mut |delta, _| {
if let Some(path) = delta.new_file().path() {
files.push(path.to_string_lossy().to_string());
}
true
},
None,
None,
None,
)?;
Ok(files)
}
fn get_total_commits(&self, repo: &Repository) -> Result<i64> {
let mut revwalk = repo.revwalk()?;
revwalk.push_head()?;
let count = revwalk.count() as i64;
Ok(count)
}
fn get_contributors(&self, repo: &Repository) -> Result<Vec<String>> {
let mut revwalk = repo.revwalk()?;
revwalk.push_head()?;
let mut contributors = std::collections::HashSet::new();
for oid in revwalk {
let oid = oid?;
let commit = repo.find_commit(oid)?;
let name = commit.author().name()
.map(|n| n.to_string());
if let Some(name) = name {
contributors.insert(name);
}
}
Ok(contributors.into_iter().collect())
}
pub fn get_commit_stats(&self, path: &Path, days: i64) -> Result<CommitStats> {
let repo = Repository::open(path)?;
let since = Utc::now() - chrono::Duration::days(days);
let mut revwalk = repo.revwalk()?;
revwalk.push_head()?;
let mut stats = CommitStats::default();
for oid in revwalk {
let oid = oid?;
let commit = repo.find_commit(oid)?;
let commit_time = commit.committer().when().seconds();
let commit_date = Utc.timestamp_opt(commit_time, 0).single();
if let Some(date) = commit_date {
if date > since {
stats.total_commits += 1;
let hour = ((commit_time / 3600) % 24) as u8;
stats.commits_by_hour.entry(hour).and_modify(|e| *e += 1).or_insert(1);
let day = date.format("%A").to_string();
stats.commits_by_day.entry(day).and_modify(|e| *e += 1).or_insert(1);
}
}
}
Ok(stats)
}
}
#[derive(Debug, Clone, Default)]
pub struct CommitStats {
pub total_commits: i64,
pub commits_by_hour: std::collections::HashMap<u8, i64>,
pub commits_by_day: std::collections::HashMap<String, i64>,
}