use crate::error::{
Error,
Result,
};
use git2::{
Commit,
DescribeOptions,
Repository as GitRepository,
Sort,
};
use glob::Pattern;
use indexmap::IndexMap;
use std::io;
use std::path::PathBuf;
pub struct Repository {
inner: GitRepository,
}
impl Repository {
pub fn init(path: PathBuf) -> Result<Self> {
if path.exists() {
Ok(Self {
inner: GitRepository::open(path)?,
})
} else {
Err(Error::IoError(io::Error::new(
io::ErrorKind::NotFound,
"repository path not found",
)))
}
}
pub fn commits(
&self,
range: Option<String>,
include_path: Option<Vec<Pattern>>,
exclude_path: Option<Vec<Pattern>>,
) -> Result<Vec<Commit>> {
let mut revwalk = self.inner.revwalk()?;
revwalk.set_sorting(Sort::TIME | Sort::TOPOLOGICAL)?;
if let Some(range) = range {
revwalk.push_range(&range)?;
} else {
revwalk.push_head()?;
}
let mut commits: Vec<Commit> = revwalk
.filter_map(|id| id.ok())
.filter_map(|id| self.inner.find_commit(id).ok())
.collect();
if include_path.is_some() || exclude_path.is_some() {
commits.retain(|commit| {
if let Ok(prev_commit) = commit.parent(0) {
if let Ok(diff) = self.inner.diff_tree_to_tree(
commit.tree().ok().as_ref(),
prev_commit.tree().ok().as_ref(),
None,
) {
return diff
.deltas()
.filter_map(|delta| delta.new_file().path())
.any(|new_file_path| {
if let Some(include_path) = &include_path {
include_path
.iter()
.any(|glob| glob.matches_path(new_file_path))
} else if let Some(exclude_path) = &exclude_path {
!exclude_path
.iter()
.any(|glob| glob.matches_path(new_file_path))
} else {
false
}
});
}
}
false
});
}
Ok(commits)
}
pub fn current_tag(&self) -> Option<String> {
self.inner
.describe(DescribeOptions::new().describe_tags())
.ok()
.and_then(|describe| describe.format(None).ok())
}
pub fn tags(
&self,
pattern: &Option<String>,
topo_order: bool,
) -> Result<IndexMap<String, String>> {
let mut tags: Vec<(Commit, String)> = Vec::new();
let tag_names = self.inner.tag_names(pattern.as_deref())?;
for name in tag_names.iter().flatten().map(String::from) {
let obj = self.inner.revparse_single(&name)?;
if let Ok(commit) = obj.clone().into_commit() {
tags.push((commit, name));
} else if let Some(tag) = obj.as_tag() {
if let Some(commit) = tag
.target()
.ok()
.and_then(|target| target.into_commit().ok())
{
tags.push((commit, name));
}
}
}
if !topo_order {
tags.sort_by(|a, b| a.0.time().seconds().cmp(&b.0.time().seconds()));
}
Ok(tags
.into_iter()
.map(|(a, b)| (a.id().to_string(), b))
.collect())
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::commit::Commit as AppCommit;
use git_conventional::ErrorKind;
use std::env;
use std::process::Command;
use std::str;
fn get_last_commit_hash() -> Result<String> {
Ok(str::from_utf8(
Command::new("git")
.args(["log", "--pretty=format:'%H'", "-n", "1"])
.output()?
.stdout
.as_ref(),
)?
.trim_matches('\'')
.to_string())
}
fn get_last_tag() -> Result<String> {
Ok(str::from_utf8(
Command::new("git")
.args(["describe", "--abbrev=0"])
.output()?
.stdout
.as_ref(),
)?
.trim()
.to_string())
}
#[test]
fn git_log() -> Result<()> {
let repository = Repository::init(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("parent directory not found")
.to_path_buf(),
)?;
let commits = repository.commits(None, None, None)?;
let last_commit =
AppCommit::from(&commits.first().expect("no commits found").clone());
assert_eq!(get_last_commit_hash()?, last_commit.id);
if let Err(e) = last_commit.into_conventional() {
match e {
Error::ParseError(e) => {
assert_eq!(ErrorKind::InvalidFormat, e.kind())
}
_ => {
unreachable!()
}
}
}
let tags = repository.tags(&None, false)?;
assert_eq!(&get_last_tag()?, tags.last().expect("no tags found").1);
Ok(())
}
}