use anyhow::Result;
use chrono;
use git2::{DiffOptions, Oid, Repository};
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ChangeType {
Added,
Deleted,
Ignored,
Modified,
Unmodified,
}
impl From<git2::Delta> for ChangeType {
fn from(value: git2::Delta) -> Self {
match value {
git2::Delta::Untracked | git2::Delta::Added => ChangeType::Added,
git2::Delta::Modified
| git2::Delta::Renamed
| git2::Delta::Copied
| git2::Delta::Typechange
| git2::Delta::Conflicted => ChangeType::Modified,
git2::Delta::Ignored | git2::Delta::Unreadable => ChangeType::Ignored,
git2::Delta::Deleted => ChangeType::Deleted,
git2::Delta::Unmodified => ChangeType::Unmodified,
}
}
}
#[derive(Debug, Clone)]
pub struct GitBasedCodeDiff {
pub file_name: String,
pub status: ChangeType,
pub old_code: String,
pub new_code: String,
}
#[derive(Debug, Clone)]
pub struct GitCommitDiff {
pub commit_message: String,
pub code_diffs: Vec<GitBasedCodeDiff>,
}
pub fn get_commit_oid(repo: &Repository, max_depth: usize) -> Result<(Oid, Oid)> {
let mut revwalk = repo.revwalk().expect("Failed to create revwalk");
revwalk.push_head().expect("Failed to push HEAD");
let mut merge_pair: Vec<(Oid, Oid)> = Vec::new();
let mut commits: Vec<Oid> = Vec::new();
for oid in revwalk.take(max_depth) {
let commit_id = oid.expect("Failed to get commit OID");
let commit = repo.find_commit(commit_id).expect("Failed to find commit");
commits.push(commit.id());
if commit.parent_count() > 1 {
for parent_index in 1..commit.parent_count() {
let parent_commit = commit
.parent(parent_index - 1)
.expect("Failed to get parent commit");
merge_pair.push((parent_commit.id(), commit.id()));
}
}
}
let (parent_oid, merged_oid) = match merge_pair.last() {
Some(x) => {
if commits.iter().any(|y| y == &x.0) && commits.iter().any(|z| z == &x.1) {
(commits.last().unwrap().to_owned(), commits[0])
} else if commits.iter().any(|y| y == &x.1) && commits.iter().any(|z| z != &x.0) {
(x.0, commits[0])
} else {
(x.0, x.1)
}
}
None => (commits.last().unwrap().to_owned(), commits[0]),
};
Ok((parent_oid, merged_oid))
}
fn get_code_diff(
repo: &Repository,
base_oid: &Oid,
compare_oid: &Oid,
) -> Result<Vec<GitBasedCodeDiff>> {
let base_commit = repo
.find_commit(*base_oid)
.expect("Failed to find old commit");
let compare_commit = repo
.find_commit(*compare_oid)
.expect("Failed to find new commit");
let base_tree = base_commit.tree().expect("Failed to get old commit tree");
let compare_tree = compare_commit
.tree()
.expect("Failed to get new commit tree");
let mut diff_options = DiffOptions::new();
let diff = repo
.diff_tree_to_tree(
Some(&base_tree),
Some(&compare_tree),
Some(&mut diff_options),
)
.expect("Failed to create diff");
let mut code_diffs: Vec<GitBasedCodeDiff> = Vec::new();
diff.deltas().for_each(|delta| {
code_diffs.push(GitBasedCodeDiff {
file_name: delta
.new_file()
.path()
.unwrap()
.to_str()
.unwrap()
.to_owned(),
status: delta.status().into(),
old_code: String::new(),
new_code: String::new(),
})
});
let mut line_callback = |delta: git2::DiffDelta<'_>,
_hunk: Option<git2::DiffHunk<'_>>,
line: git2::DiffLine<'_>| {
match code_diffs.iter_mut().find(|item| {
item.file_name
== delta
.new_file()
.path()
.unwrap()
.to_str()
.unwrap()
.to_owned()
}) {
Some(code_diff) => {
let line_content = std::str::from_utf8(line.content()).unwrap_or_default();
match line.origin_value() {
git2::DiffLineType::Context => {
code_diff.old_code.push_str(&line_content);
code_diff.new_code.push_str(&line_content);
}
git2::DiffLineType::Addition => {
code_diff.new_code.push_str(&line_content);
}
git2::DiffLineType::Deletion => {
code_diff.old_code.push_str(&line_content);
}
_ => {}
}
}
None => {}
}
true
};
diff.foreach(
&mut |_: git2::DiffDelta<'_>, _: f32| true,
None,
None,
Some(&mut line_callback),
)
.expect("Failed to iterate through diff entries");
Ok(code_diffs)
}
fn get_commit_messages(repo: &Repository, base_oid: &Oid, compare_oid: &Oid) -> Result<String> {
let mut revwalk = repo.revwalk().expect("Failed to create revision walker");
revwalk
.set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::REVERSE)
.expect("Failed to set sorting order");
revwalk
.push(*compare_oid)
.expect("Failed to push ending commit");
revwalk
.hide(*base_oid)
.expect("Failed to hide starting commit");
let mut commit_messages: Vec<String> = Vec::new();
for oid in revwalk {
let oid = oid.expect("Error getting OID");
let commit = repo.find_commit(oid).expect("Error finding commit");
commit_messages.push(commit.summary().unwrap_or("No summary").to_string());
}
Ok(commit_messages.join("\n"))
}
pub fn get_commit_diff(
repo: &Repository,
base_oid: &Oid,
compare_oid: &Oid,
) -> Result<GitCommitDiff> {
let commit_messages = get_commit_messages(repo, base_oid, compare_oid)?;
let code_diffs = get_code_diff(repo, base_oid, compare_oid)?;
Ok(GitCommitDiff {
commit_message: commit_messages,
code_diffs,
})
}
pub fn get_commit_timestamps(
repo: &Repository,
base_oid: &Oid,
compare_oid: &Oid,
) -> Result<(chrono::DateTime<chrono::Utc>, chrono::DateTime<chrono::Utc>)> {
let base_commit = repo
.find_commit(*base_oid)
.expect("Failed to find old commit");
let compare_commit = repo
.find_commit(*compare_oid)
.expect("Failed to find new commit");
let base_timestamp = base_commit.time();
let base_timestamp_in_seconds = base_timestamp.seconds();
let _offset_minutes = base_timestamp.offset_minutes();
let _offset_sign = match base_timestamp.sign() {
'+' => 1,
'-' => -1,
_ => 0,
};
let base_datetime = chrono::DateTime::from_timestamp(base_timestamp_in_seconds as i64, 0);
let compare_timestamp = compare_commit.time();
let compare_timestamp_in_seconds = compare_timestamp.seconds();
let _offset_minutes = compare_timestamp.offset_minutes();
let _offset_sign = match compare_timestamp.sign() {
'+' => 1,
'-' => -1,
_ => 0,
};
let compare_datetime = chrono::DateTime::from_timestamp(compare_timestamp_in_seconds as i64, 0);
Ok((base_datetime.unwrap(), compare_datetime.unwrap()))
}
pub fn get_total_commit_count(repo: &Repository) -> Result<usize> {
let mut revwalk = repo.revwalk().expect("Failed to create revwalk");
revwalk.push_head().expect("Failed to push HEAD");
Ok(revwalk.count())
}