use std::path::{Path, PathBuf};
use crate::config::Config;
use crate::error::{Result, TagVerError};
use crate::tags::{parse_tags, TagMap, VersionTag};
use crate::version::Version;
pub struct Repository {
inner: gix::Repository,
is_shallow: bool,
}
impl Repository {
pub fn discover(path: impl Into<PathBuf>) -> Result<Self> {
let path = path.into();
let repo = gix::discover(&path)
.map_err(|e| TagVerError::GitRepoNotFound(format!("{}: {}", path.display(), e)))?;
let is_shallow = repo.is_shallow();
Ok(Self {
inner: repo,
is_shallow,
})
}
pub fn is_shallow(&self) -> bool {
self.is_shallow
}
pub fn work_dir(&self) -> Option<&Path> {
self.inner.workdir()
}
pub fn inner(&self) -> &gix::Repository {
&self.inner
}
}
pub fn calculate_version(repo: &Repository, config: &Config) -> Result<(Version, u32, bool)> {
let (tag_map, _invalid_tags) = parse_tags(repo.inner(), config)?;
let mut head = repo
.inner()
.head()
.map_err(|e| TagVerError::Other(format!("Failed to get HEAD: {}", e)))?;
let head_commit = match head.try_peel_to_id() {
Ok(Some(id)) => id.detach(),
Ok(None) | Err(_) => {
let version = Version::default(&config.default_prerelease_identifiers);
let version = apply_config(version, config, None, 0);
return Ok((version, 0, false));
}
};
let (base_tag, height) = walk_to_tag(repo.inner(), head_commit, &tag_map)?;
let effective_height = if config.ignore_height { 0 } else { height };
let (version, is_from_tag) = match base_tag {
Some(ref tag) => {
let synthesized = synthesize_version(&tag.version, effective_height, config);
(synthesized, height == 0)
}
None => {
let default = Version::default(&config.default_prerelease_identifiers);
let version = if effective_height > 0 {
let mut v = default;
v.prerelease.push(effective_height.to_string());
v
} else {
default
};
(version, false)
}
};
let final_version = apply_config(version, config, base_tag.as_ref(), height);
Ok((final_version, height, is_from_tag))
}
fn walk_to_tag(
repo: &gix::Repository,
start: gix::ObjectId,
tag_map: &TagMap,
) -> Result<(Option<VersionTag>, u32)> {
let mut height: u32 = 0;
let mut current = start;
loop {
if let Some(tags) = tag_map.get(¤t) {
if let Some(tag) = tags.first() {
return Ok((Some(tag.clone()), height));
}
}
let commit = match repo.find_object(current) {
Ok(obj) => match obj.try_into_commit() {
Ok(c) => c,
Err(_) => break,
},
Err(_) => break,
};
let parents: Vec<_> = commit.parent_ids().collect();
if parents.is_empty() {
break;
}
current = parents[0].detach();
height += 1;
}
Ok((None, height))
}
fn synthesize_version(base: &Version, height: u32, config: &Config) -> Version {
if height == 0 {
return base.clone();
}
if base.is_prerelease() {
base.with_prerelease_height(height)
} else {
base.with_rtm_height(
height,
&config.auto_increment,
&config.default_prerelease_identifiers,
)
}
}
fn apply_config(
mut version: Version,
config: &Config,
tag: Option<&VersionTag>,
height: u32,
) -> Version {
if let Some(ref min) = config.minimum_major_minor {
if height > 0 || tag.is_none() {
version = version.apply_minimum(min, &config.default_prerelease_identifiers);
}
}
let tag_metadata = tag.and_then(|t| t.version.build_metadata.as_deref());
let config_metadata = config.build_metadata.as_deref();
if tag_metadata.is_some() || config_metadata.is_some() {
let effective_tag_metadata = if height == 0 { tag_metadata } else { None };
version = version.with_merged_build_metadata(effective_tag_metadata, config_metadata);
}
version
}
pub fn calculate_version_fallback(
work_dir: impl Into<PathBuf>,
config: &Config,
) -> Result<(Version, u32, bool)> {
let work_dir = work_dir.into();
match Repository::discover(&work_dir) {
Ok(repo) => calculate_version(&repo, config),
Err(TagVerError::GitRepoNotFound(_)) => {
let version = Version::default(&config.default_prerelease_identifiers);
let version = apply_config(version, config, None, 0);
Ok((version, 0, false))
}
Err(e) => Err(e),
}
}
pub fn is_git_directory(path: impl Into<PathBuf>) -> bool {
let path = path.into();
gix::discover(path).is_ok()
}