use chrono::DateTime;
use git2::{self, Repository};
use regex::Regex;
use std::convert::TryFrom;
use std::path::Path;
use std::path::PathBuf;
use std::str;
use std::sync::LazyLock;
use thiserror::Error;
use crate::var::Key;
#[derive(Error, Debug)]
#[error("Git2 lib error: {from} - {message}")]
pub struct Error {
from: git2::Error,
message: String,
}
impl From<&str> for Error {
fn from(message: &str) -> Self {
Self {
from: git2::Error::from_str("PLACEHOLDER"),
message: String::from(message),
}
}
}
pub const DATE_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
#[derive(Clone, Copy)]
pub enum TransferProtocol {
Git,
Https,
Ssh,
}
impl TransferProtocol {
#[must_use]
pub const fn scheme_str(self) -> &'static str {
match self {
Self::Git => "git",
Self::Https => "https",
Self::Ssh => "ssh",
}
}
#[must_use]
pub const fn to_clone_url_key(self) -> Key {
match self {
Self::Git => Key::RepoCloneUrlGit,
Self::Https => Key::RepoCloneUrlHttp,
Self::Ssh => Key::RepoCloneUrlSsh,
}
}
}
#[must_use]
pub fn is_git_broken_version(vers: &str) -> bool {
static R_BROKEN_VERSION: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[^-].+(-dirty)?-broken(-.+)?$").unwrap());
R_BROKEN_VERSION.is_match(vers)
}
#[must_use]
pub fn is_git_dirty_version(vers: &str) -> bool {
static R_DIRTY_VERSION: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[^-].+(-broken)?-dirty(-.+)?$").unwrap());
R_DIRTY_VERSION.is_match(vers)
}
fn _has_tags(repo: &git2::Repository) -> bool {
let mut has_tags = false;
let _ = repo.tag_foreach(|_, _| {
has_tags = true;
false
});
has_tags
}
fn _version(repo: &git2::Repository) -> Result<String, Error> {
repo.describe(
git2::DescribeOptions::new()
.pattern("*[0-9]*.[0-9]*.[0-9]*")
.describe_tags(),
)
.map_err(|from| Error {
from,
message: String::from("Failed to describe the HEAD revision version"),
})?
.format(Some(
git2::DescribeFormatOptions::new()
.always_use_long_format(false)
.dirty_suffix("-dirty"),
))
.map_err(|from| Error {
from,
message: String::from("Failed to format the HEAD revision version"),
})
}
pub struct Repo {
repo: git2::Repository,
}
impl TryFrom<Option<&str>> for Repo {
type Error = git2::Error;
fn try_from(repo_root: Option<&str>) -> Result<Self, Self::Error> {
let repo = Repository::open(repo_root.unwrap_or("."))?;
Ok(Self { repo })
}
}
impl TryFrom<Option<&Path>> for Repo {
type Error = git2::Error;
fn try_from(repo_root: Option<&Path>) -> Result<Self, Self::Error> {
let repo = Repository::open(repo_root.unwrap_or_else(|| Path::new(".")))?;
Ok(Self { repo })
}
}
impl Repo {
#[must_use]
pub const fn inner(&self) -> &git2::Repository {
&self.repo
}
#[must_use]
pub fn local_path(&self) -> PathBuf {
let path = self.repo.path().canonicalize().unwrap(); match path.file_name() {
Some(file_name) => {
if file_name.to_str().unwrap() == ".git" {
path.parent().unwrap().to_path_buf() } else {
path
}
}
None => {
Path::new("/").to_path_buf()
}
}
}
#[must_use]
pub fn local_path_str(&self) -> String {
self.local_path().to_str().unwrap().to_owned()
}
fn _branch(&self) -> Result<Option<git2::Branch<'_>>, Error> {
let head_ref = self.repo.head().map_err(|from| Error {
from,
message: String::from("Failed to convert HEAD into a branch"),
})?;
Ok(if head_ref.is_branch() {
Some(git2::Branch::wrap(head_ref))
} else {
log::warn!(
"Failed to get the current branch.
This may indicate either:
* valid: No branch is checked out
-> HEAD is pointing to a commit or a tag
* problem: You are running on CI,
and while it should have a branch checked out,
it has not.
This may happen with shallow repos,
see for example GitLab bug
<https://gitlab.com/gitlab-org/gitlab/-/issues/350100>."
);
None
})
}
pub fn sha(&self) -> Result<Option<String>, Error> {
let head = self.repo.head().map_err(|from| Error {
from,
message: String::from("Failed to get repo HEAD for figuring out the SHA1"),
})?;
Ok(
head.resolve()
.map_err(|from| Error {
from,
message: String::from("Failed resolving HEAD into a direct reference"),
})?
.target()
.map(|oid| oid.to_string()),
) }
pub fn branch(&self) -> Result<Option<String>, Error> {
Ok(if let Some(branch) = self._branch()? {
Some(
branch
.name()
.map_err(|from| Error {
from,
message: String::from("Failed fetching name of a branch"),
})?
.ok_or_else(|| Error::from("Branch name is not UTF-8 compatible"))?
.to_owned(),
)
} else {
None
})
}
fn _tag(&self) -> Result<Option<String>, Error> {
let head = self.repo.head().map_err(|from| Error {
from,
message: String::from("Failed to get repo HEAD for figuring out the tag"),
})?;
let head_oid = head
.resolve()
.map_err(|from| Error {
from,
message: String::from("Failed resolve HEAD into a reference"),
})?
.target()
.ok_or_else(|| git2::Error::from_str("No OID for HEAD"))
.map_err(|from| Error {
from,
message: String::from("-"),
})?;
let mut tag = None;
let mut inner_err: Option<Result<Option<String>, Error>> = None;
self.repo
.tag_foreach(|_id, name| {
let name_str = String::from_utf8(name.to_vec())
.expect("Failed to convert tag name to UTF-8 string");
let cur_tag_res = self.repo.find_reference(&name_str).and_then(|git_ref| {
git_ref.target().ok_or_else(|| {
git2::Error::from_str("Failed to get tag reference target commit")
})
});
let cur_tag = match cur_tag_res {
Err(from) => {
inner_err = Some(Err(Error {
from,
message: String::from("Failed fetching current tag reference"),
}));
return false;
}
Ok(cur_tag) => cur_tag,
};
if cur_tag == head_oid {
tag = Some(name_str);
false
} else {
true
}
})
.map_err(|from| Error {
from,
message: String::from("Failed processing tags"),
})?;
match inner_err {
Some(err) => err,
None => Ok(tag),
}
}
pub fn tag(&self) -> Result<Option<String>, Error> {
self._tag()
}
fn _remote_tracking_branch(&self) -> Result<Option<git2::Branch<'_>>, Error> {
if let Some(branch) = self._branch()? {
match branch.upstream() {
Ok(remote_branch) => Ok(Some(remote_branch)),
Err(from) => {
if from.code() == git2::ErrorCode::NotFound
{
Ok(None)
} else {
Err(Error {
from,
message: String::from("Failed resolving the remote tracking branch"),
})
}
}
}
} else {
Ok(None)
}
}
pub fn remote_tracking_branch(&self) -> Result<Option<String>, Error> {
Ok(
if let Some(remote_tracking_branch) = self._remote_tracking_branch()? {
Some(
remote_tracking_branch
.name()
.map_err(|from| Error {
from,
message: String::from(
"Failed fetching the remote tracking branch name",
),
})?
.ok_or_else(|| {
Error::from("Remote tracking branch name is not UTF-8 compatible")
})?
.to_owned(),
)
} else {
None
},
)
}
pub fn remote_name(&self) -> Result<Option<String>, Error> {
Ok(
if let Some(remote_tracking_branch) = self.remote_tracking_branch()? {
Some(self
.repo
.branch_remote_name(
self.repo
.resolve_reference_from_short_name(&remote_tracking_branch)
.map_err(|from| Error {
from,
message: String::from(
"Failed to resolve reference from remote-tracking branch short name",
),
})?
.name()
.ok_or_else(|| Error::from("Remote branch name is not UTF-8 compatible"))?,
)
.map_err(|from| Error {
from,
message: String::from("Failed to get branch remote name"),
})?
.as_str()
.ok_or_else(|| Error::from("Remote name is not UTF-8 compatible"))?
.to_owned())
} else {
None
},
)
}
pub fn remote_clone_url(&self) -> Result<Option<String>, Error> {
Ok(if let Some(remote_name) = self.remote_name()? {
Some(
self.repo
.find_remote(&remote_name)
.map_err(|from| Error {
from,
message: String::from("Failed to find remote name for remote clone URL"),
})?
.url()
.ok_or_else(|| Error::from("Remote URL is not UTF-8 compatible"))?
.to_owned(),
)
} else {
None
})
}
pub fn version(&self) -> Result<String, Error> {
if _has_tags(&self.repo) {
_version(&self.repo)
} else {
log::warn!(
"The git repository has no tags.
Please consider adding at least a tag '0.1.0' to the first commit of the repo history; \
for example with:
git tag -a -m 'Release 0.1.0' 0.1.0 $(git rev-list --max-parents=0 HEAD)"
);
match self.sha()? {
Some(sha_str) => Ok(sha_str),
None => Err(Error::from(
"The repo has no tags, so we can not use git describe, \
and there is no commit checked out either",
)),
}
}
}
pub fn commit_date(&self, date_format: &str) -> Result<String, Error> {
let head = self.repo.head().map_err(|from| Error {
from,
message: String::from("Failed to get repo HEAD for figuring out the commit date"),
})?;
let commit_time_git2 = head
.peel_to_commit()
.map_err(|from| Error {
from,
message: String::from(
"Failed to peal HEAD to commit for figuring out the commit date",
),
})?
.time();
let commit_time_chrono = DateTime::from_timestamp(commit_time_git2.seconds(), 0)
.ok_or_else(|| {
Error::from("Failed to peal HEAD to commit for figuring out the commit date")
})?;
Ok(commit_time_chrono.format(date_format).to_string())
}
}