diff --git a/src/git/commit.rs b/src/git/commit.rs new file mode 100644 index 0000000..0cf1c4d --- /dev/null +++ b/src/git/commit.rs @@ -0,0 +1,145 @@ +use anyhow::Result; +use chrono::{DateTime, TimeZone, Utc}; +use git2; + +pub struct Commit<'a> { + commit: git2::Commit<'a>, +} + +impl<'a> Commit<'a> { + pub fn new(commit: git2::Commit<'a>) -> Self { + Self { commit } + } + + pub fn id(&self) -> git2::Oid { + self.commit.id() + } + + pub fn hex(&self) -> String { + self.id().to_string() + } + + pub fn short_id(&self, len: usize) -> String { + self.id().to_string()[..len.min(40)].to_string() + } + + pub fn message(&self) -> Option<&str> { + self.commit.message() + } + + pub fn message_short(&self, max_len: usize) -> String { + let msg = self.commit.message().unwrap_or(""); + if msg.len() > max_len { + format!("{}...", &msg[..max_len - 3]) + } else { + msg.to_string() + } + } + + pub fn summary(&self) -> Option<&str> { + self.commit.summary() + } + + pub fn time(&self) -> DateTime { + let time = self.commit.time(); + let naive = chrono::NaiveDateTime::from_timestamp_opt(time.seconds(), 0).unwrap(); + Utc.from_utc_datetime(&naive) + } + + pub fn author(&self) -> &git2::Signature { + self.commit.author() + } + + pub fn author_name(&self) -> String { + self.commit.author().name().unwrap_or("Unknown").to_string() + } + + pub fn author_email(&self) -> String { + self.commit.author().email().unwrap_or("Unknown").to_string() + } + + pub fn committer(&self) -> &git2::Signature { + self.commit.committer() + } + + pub fn committer_name(&self) -> String { + self.commit.committer().name().unwrap_or("Unknown").to_string() + } + + pub fn committer_email(&self) -> String { + self.commit.committer().email().unwrap_or("Unknown").to_string() + } + + pub fn parent_count(&self) -> usize { + self.commit.parent_count() + } + + pub fn parents(&self) -> Vec> { + self.commit.parents().collect() + } + + pub fn is_merge(&self) -> bool { + self.commit.parent_count() > 1 + } + + pub fn is_initial_commit(&self) -> bool { + self.commit.parent_count() == 0 + } + + pub fn tree(&self) -> Result { + self.commit.tree().with_context(|| "Failed to get tree") + } + + pub fn diff_to_parent(&self) -> Result> { + if self.is_initial_commit() { + return Ok(None); + } + let parent = self.commit.parent(0)?; + let parent_tree = parent.tree()?; + let commit_tree = self.commit.tree()?; + let repo = self.commit.repository(); + let diff = repo.diff_tree_to_tree(Some(&parent_tree), Some(&commit_tree), None)?; + Ok(Some(diff)) + } + + pub fn raw(&self) -> &git2::Commit<'a> { + &self.commit + } +} + +pub struct CommitIterator<'a> { + repo: &'a git2::Repository, + revwalk: git2::Revwalk<'a>, +} + +impl<'a> CommitIterator<'a> { + pub fn new(repo: &'a git2::Repository) -> Result { + let mut revwalk = repo.revwalk()?; + revwalk.set_sorting(git2::Sort::TIME)?; + revwalk.push_head()?; + Ok(Self { repo, revwalk }) + } + + pub fn with_sorting(repo: &'a git2::Repository, sorting: git2::Sort) -> Result { + let mut revwalk = repo.revwalk()?; + revwalk.set_sorting(sorting)?; + revwalk.push_head()?; + Ok(Self { repo, revwalk }) + } +} + +impl<'a> Iterator for CommitIterator<'a> { + type Item = Result>; + + fn next(&mut self) -> Option { + self.revwalk + .next() + .map(|oid| match oid { + Ok(oid) => match self.repo.find_commit(oid) { + Ok(commit) => Ok(Commit::new(commit)), + Err(e) => Err(anyhow::anyhow!("Failed to find commit {}: {}", oid, e)), + }, + Err(e) => Err(anyhow::anyhow!("Revwalk error: {}", e)), + }) + } +}