use std::{cell::Cell, path::Path, str::FromStr};
use git2::{Diff, Repository, Sort};
use serde::Serialize;
use crate::errors::TeacupError;
#[derive(Serialize, Clone, Debug)]
struct FileChange {
path: String,
lines_added: u32,
lines_removed: u32,
lines_modified: u32,
hunks_added: u32,
hunks_removed: u32,
hunks_modified: u32,
}
#[derive(Serialize, Debug, Clone)]
pub enum CommitType {
Normal,
Merge,
}
#[derive(Serialize, Debug)]
struct Commit {
id: String,
parents: Vec<String>,
repo_url: String,
timestamp: i64,
author_name: String,
author_email: String,
message: String,
r#type: CommitType,
changes: Vec<FileChange>,
}
#[derive(Serialize, Debug)]
pub struct FlatCommit {
id: String,
parents: Vec<String>,
repo_url: String,
timestamp: i64,
author_name: String,
author_email: String,
message: String,
r#type: CommitType,
path: String,
lines_added: u32,
lines_removed: u32,
lines_modified: u32,
hunks_added: u32,
hunks_removed: u32,
hunks_modified: u32,
}
impl From<git2::Error> for TeacupError {
fn from(err: git2::Error) -> Self {
TeacupError::GitError(err.to_string())
}
}
fn extract_from_diff(diff: &Diff) -> Result<Vec<FileChange>, TeacupError> {
let mut files: Vec<FileChange> = Vec::new();
let x: Cell<Option<FileChange>> = Cell::new(None);
diff.foreach(
&mut |diff_delta, _s| {
match x.take() {
Some(file_change) => {
files.push(file_change);
}
_ => {}
}
let filename = diff_delta.new_file().path().unwrap().to_str().unwrap();
x.set(Some(FileChange {
path: String::from_str(filename).unwrap(),
lines_added: 0,
lines_removed: 0,
lines_modified: 0,
hunks_added: 0,
hunks_removed: 0,
hunks_modified: 0,
}));
true
},
None,
Some(&mut |_diff_delta, diff_hunk| {
let state = x.take().unwrap();
let updated = match (diff_hunk.old_lines(), diff_hunk.new_lines()) {
(0, _) => FileChange {
hunks_added: state.hunks_added + 1,
..state
},
(_, 0) => FileChange {
hunks_removed: state.hunks_removed + 1,
..state
},
(_, _) => FileChange {
hunks_modified: state.hunks_modified + 1,
..state
},
};
x.set(Some(updated));
true
}),
Some(&mut |_diff_delta, _diff_hunk, diff_line| {
let state = x.take().unwrap();
let updated = match (diff_line.old_lineno(), diff_line.new_lineno()) {
(None, Some(_)) => FileChange {
lines_added: state.lines_added + 1,
..state
},
(Some(_), None) => FileChange {
lines_removed: state.lines_removed + 1,
..state
},
(Some(_), Some(_)) => FileChange {
lines_modified: state.lines_modified + 1,
..state
},
_ => state,
};
x.set(Some(updated));
true
}),
)?;
Ok(files)
}
pub fn extract_logs(repo_location: String) -> Result<Vec<FlatCommit>, TeacupError> {
let repo = Repository::open(repo_location.clone())?;
let mut revwalk = repo.revwalk()?;
revwalk.set_sorting(Sort::TIME)?;
revwalk.push_head()?;
let branches = repo.branches(None)?;
for branch_r in branches {
if let Ok((branch, _branch_type)) = branch_r {
if !branch.is_head() {
if let Some(target) = branch.get().target() {
revwalk.push(target)?;
} else {
}
}
}
}
let mut result: Vec<FlatCommit> = vec![];
while let Some(Ok(oid)) = revwalk.next() {
let commit = repo.find_commit(oid)?;
let commit_tree = repo.find_tree(commit.tree_id()).unwrap();
let parent_commit = if commit.parent_count() == 0 {
None
} else {
Some(commit.parent(0).unwrap().tree_id())
};
let parent_tree = parent_commit.map(|oid| repo.find_tree(oid).unwrap());
let default_commit = Commit {
id: oid.to_string(),
parents: commit.parents().map(|p| p.id().to_string()).collect(),
r#type: CommitType::Normal,
repo_url: repo_location.to_string(),
timestamp: commit.time().seconds(),
author_name: commit.author().name().unwrap_or("unknown").to_string(),
author_email: commit.author().email().unwrap_or("unknown").to_string(),
message: commit.message().unwrap_or("unknown").to_string(),
changes: Vec::new(),
};
let my_commit = if commit.parent_count() > 1 {
Commit {
r#type: CommitType::Merge,
..default_commit
}
}
else {
let diff = repo.diff_tree_to_tree(parent_tree.as_ref(), Some(&commit_tree), None)?;
let file_changes = extract_from_diff(&diff)?;
Commit {
r#type: CommitType::Normal,
changes: file_changes,
..default_commit
}
};
let flats: Vec<FlatCommit> = my_commit
.changes
.iter()
.map(|change| FlatCommit {
id: my_commit.id.clone(),
parents: my_commit.parents.clone(),
r#type: my_commit.r#type.clone(),
repo_url: my_commit.repo_url.clone(),
timestamp: my_commit.timestamp.clone(),
author_name: my_commit.author_name.clone(),
author_email: my_commit.author_email.clone(),
message: my_commit.message.clone(),
path: change.path.clone(),
lines_added: change.lines_added.clone(),
lines_removed: change.lines_removed.clone(),
lines_modified: change.lines_modified.clone(),
hunks_added: change.hunks_added.clone(),
hunks_removed: change.hunks_removed.clone(),
hunks_modified: change.hunks_modified.clone(),
})
.collect();
for flat in flats {
result.push(flat);
}
}
Ok(result)
}
pub fn is_git_repo(location: &String) -> bool {
Path::new(location).exists() && Repository::open(location).is_ok()
}