use crate::config::Remote;
use crate::error::{
Error,
Result,
};
use git2::{
BranchType,
Commit,
DescribeOptions,
Oid,
Repository as GitRepository,
Sort,
};
use glob::Pattern;
use indexmap::IndexMap;
use regex::Regex;
use std::io;
use std::path::PathBuf;
use url::Url;
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::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 find_commit(&self, id: String) -> Option<Commit> {
if let Ok(oid) = Oid::from_str(&id) {
if let Ok(commit) = self.inner.find_commit(oid) {
return Some(commit);
}
}
None
}
pub fn tags(
&self,
pattern: &Option<Regex>,
topo_order: bool,
) -> Result<IndexMap<String, String>> {
let mut tags: Vec<(Commit, String)> = Vec::new();
let tag_names = self.inner.tag_names(None)?;
for name in tag_names
.iter()
.flatten()
.filter(|tag_name| {
pattern.as_ref().map_or(true, |pat| pat.is_match(tag_name))
})
.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())
}
pub fn upstream_remote(&self) -> Result<Remote> {
for branch in self.inner.branches(Some(BranchType::Local))? {
let branch = branch?.0;
if branch.is_head() {
let upstream = &self.inner.branch_upstream_remote(&format!(
"refs/heads/{}",
&branch.name()?.ok_or_else(|| Error::RepoError(
String::from("branch name is not valid")
))?
))?;
let upstream_name = upstream.as_str().ok_or_else(|| {
Error::RepoError(String::from(
"name of the upstream remote is not valid",
))
})?;
let origin = &self.inner.find_remote(upstream_name)?;
let url = origin
.url()
.ok_or_else(|| {
Error::RepoError(String::from(
"failed to get the remote URL",
))
})?
.to_string();
trace!("Upstream URL: {url}");
let url = Url::parse(&url)?;
let segments: Vec<&str> = url
.path_segments()
.ok_or_else(|| {
Error::RepoError(String::from("failed to get URL segments"))
})?
.rev()
.collect();
if let (Some(owner), Some(repo)) =
(segments.get(1), segments.first())
{
return Ok(Remote {
owner: owner.to_string(),
repo: repo.trim_end_matches(".git").to_string(),
token: None,
});
}
}
}
Err(Error::RepoError(String::from("no remotes configured")))
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::commit::Commit as AppCommit;
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())
}
fn get_repository() -> Result<Repository> {
Repository::init(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("parent directory not found")
.to_path_buf(),
)
}
#[test]
fn get_latest_commit() -> Result<()> {
let repository = get_repository()?;
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);
Ok(())
}
#[test]
fn get_latest_tag() -> Result<()> {
let repository = get_repository()?;
let tags = repository.tags(&None, false)?;
assert_eq!(&get_last_tag()?, tags.last().expect("no tags found").1);
Ok(())
}
#[test]
fn git_tags() -> Result<()> {
let repository = get_repository()?;
let tags = repository.tags(&None, true)?;
assert_eq!(
tags.get("2b8b4d3535f29231e05c3572e919634b9af907b6").expect(
"the commit hash does not exist in the repository (tag v0.1.0)"
),
"v0.1.0"
);
assert_eq!(
tags.get("4ddef08debfff48117586296e49d5caa0800d1b5").expect(
"the commit hash does not exist in the repository (tag \
v0.1.0-beta.4)"
),
"v0.1.0-beta.4"
);
let tags = repository.tags(
&Some(
Regex::new("^v[0-9]+\\.[0-9]+\\.[0-9]$")
.expect("the regex is not valid"),
),
true,
)?;
assert_eq!(
tags.get("2b8b4d3535f29231e05c3572e919634b9af907b6").expect(
"the commit hash does not exist in the repository (tag v0.1.0)"
),
"v0.1.0"
);
assert!(!tags.contains_key("4ddef08debfff48117586296e49d5caa0800d1b5"));
Ok(())
}
#[test]
fn git_upstream_remote() -> Result<()> {
let repository = get_repository()?;
let remote = repository.upstream_remote()?;
assert_eq!(
Remote {
owner: String::from("orhun"),
repo: String::from("git-cliff"),
token: None,
},
remote
);
Ok(())
}
}