use std::cmp::Reverse;
use std::collections::BinaryHeap;
use std::sync::atomic::AtomicBool;
use crate::refs::{RefName, Target};
use crate::{
HistoryError, IdMap, IdSet, ObjectId, ObjectIdPrefix, ObjectKind, Objects, PeelError,
PeelFailure, PeelLimits, ReadCommitError, Repository,
};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Candidates {
#[default]
AnnotatedTags,
Tags,
All,
}
#[derive(Clone, Debug)]
pub struct DescribeOptions {
candidates: Candidates,
matches: Vec<Vec<u8>>,
excludes: Vec<Vec<u8>>,
max_candidates: usize,
first_parent: bool,
limits: crate::HistoryLimits,
}
impl Default for DescribeOptions {
fn default() -> Self {
Self::new()
}
}
impl DescribeOptions {
pub fn new() -> Self {
Self {
candidates: Candidates::AnnotatedTags,
matches: Vec::new(),
excludes: Vec::new(),
max_candidates: 10,
first_parent: false,
limits: crate::HistoryLimits::default(),
}
}
pub fn with_limits(mut self, limits: crate::HistoryLimits) -> Self {
self.limits = limits;
self
}
pub fn with_candidates_from(mut self, candidates: Candidates) -> Self {
self.candidates = candidates;
self
}
pub fn with_lightweight_tags(self) -> Self {
self.with_candidates_from(Candidates::Tags)
}
pub fn with_match(mut self, pattern: impl Into<Vec<u8>>) -> Self {
self.matches.push(pattern.into());
self
}
pub fn with_exclude(mut self, pattern: impl Into<Vec<u8>>) -> Self {
self.excludes.push(pattern.into());
self
}
pub fn with_candidates(mut self, count: usize) -> Self {
self.max_candidates = count;
self
}
pub fn with_first_parent(mut self, first_parent: bool) -> Self {
self.first_parent = first_parent;
self
}
fn patterns_allow(&self, name: &[u8]) -> bool {
let matches =
|pattern: &Vec<u8>| crate::wildmatch::matches_without_pathname(pattern, name, 1 << 20);
if self
.excludes
.iter()
.any(|pattern| matches(pattern) == Some(true))
{
return false;
}
self.matches.is_empty()
|| self
.matches
.iter()
.any(|pattern| matches(pattern) == Some(true))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct Description {
pub name: Vec<u8>,
pub reference: RefName,
pub tagged: ObjectId,
pub distance: usize,
pub commit: ObjectId,
pub renamed: bool,
}
impl Description {
pub fn format(&self, abbreviated: Option<&ObjectIdPrefix>, long: bool) -> Vec<u8> {
let mut text = self.name.clone();
let id = match abbreviated {
Some(abbreviated) => abbreviated.to_string(),
None if self.renamed => self.commit.to_string(),
None => return text,
};
if long || self.renamed || self.distance != 0 {
text.extend_from_slice(format!("-{}-g{id}", self.distance).as_bytes());
}
text
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum DescribeError {
#[error(transparent)]
References(#[from] crate::refs::ReferenceError),
#[error("peeling {}: {source}", String::from_utf8_lossy(reference.as_bytes()))]
Peel {
reference: RefName,
#[source]
source: Box<PeelError>,
},
#[error(transparent)]
Tag(#[from] crate::ReadTagError),
#[error(transparent)]
History(#[from] HistoryError),
}
#[derive(Clone, Debug)]
struct Named {
name: Vec<u8>,
reference: RefName,
rank: u8,
tagger_seconds: i64,
renamed: bool,
}
impl Named {
fn preferred_over(&self, other: &Self) -> bool {
(self.rank, Reverse(self.tagger_seconds), &self.name)
< (other.rank, Reverse(other.tagger_seconds), &other.name)
}
}
impl Repository {
pub fn describe(
&self,
objects: &Objects,
commit: ObjectId,
options: &DescribeOptions,
) -> Result<Option<Description>, DescribeError> {
let names = candidate_names(self, objects, options)?;
let describe = |tagged: ObjectId, distance| {
names.get(&tagged).map(|named| Description {
name: named.name.clone(),
reference: named.reference.clone(),
tagged,
distance,
commit,
renamed: named.renamed,
})
};
if names.contains_key(&commit) {
return Ok(describe(commit, 0));
}
if options.max_candidates == 0 {
return Ok(None);
}
let found = newest_first(objects, commit, options, |id| names.contains_key(&id))?;
let mut best: Option<(usize, ObjectId)> = None;
for (tagged, steps) in found {
let distance = match steps {
Some(steps) => steps,
None => objects
.walk_range_with(&[commit], &[tagged], options.limits)?
.len(),
};
if best.is_none_or(|(best, _)| distance < best) {
best = Some((distance, tagged));
}
}
Ok(best.and_then(|(distance, tagged)| describe(tagged, distance)))
}
}
fn candidate_names(
repository: &Repository,
objects: &Objects,
options: &DescribeOptions,
) -> Result<IdMap<Named>, DescribeError> {
let references = repository.references()?;
let listed = match options.candidates {
Candidates::All => references.list()?,
Candidates::AnnotatedTags | Candidates::Tags => references.list_prefix("refs/tags/")?,
};
let mut names: IdMap<Named> = IdMap::default();
for reference in listed {
let id = match &reference.target {
Target::Direct(id) => *id,
Target::Symbolic(_) => match references.resolve(&reference.name)?.id {
Some(id) => id,
None => continue,
},
};
let full = reference.name.as_bytes();
let Some(name) = display_name(full, options) else {
continue;
};
let peeled = match objects.peel(id, PeelLimits::default(), &AtomicBool::new(false)) {
Ok(peeled) => peeled,
Err(error) if matches!(*error.source, PeelFailure::Missing) => continue,
Err(source) => {
return Err(DescribeError::Peel {
reference: reference.name,
source: Box::new(source),
});
}
};
if peeled.kind != ObjectKind::Commit {
continue;
}
let tag = full.starts_with(b"refs/tags/");
let annotated = tag && !peeled.tags.is_empty();
if options.candidates == Candidates::AnnotatedTags && !annotated {
continue;
}
let (tagger_seconds, renamed_to) = if annotated {
let tag = objects.read_tag(id)?;
let seconds = tag
.tagger()
.ok()
.flatten()
.and_then(|tagger| tagger.date().ok())
.map_or(0, |date| date.seconds);
let short = &full[b"refs/tags/".len()..];
let renamed = (tag.name() != short).then(|| tag.name().to_vec());
(seconds, renamed)
} else {
(0, None)
};
let renamed = renamed_to.is_some();
let name = match renamed_to {
Some(tag_name) if options.candidates == Candidates::All => {
[b"tags/".as_slice(), &tag_name].concat()
}
Some(tag_name) => tag_name,
None => name,
};
let named = Named {
name,
reference: reference.name,
rank: if annotated {
0
} else if tag {
1
} else {
2
},
tagger_seconds,
renamed,
};
match names.get(&peeled.target) {
Some(existing) if !named.preferred_over(existing) => {}
_ => {
names.insert(peeled.target, named);
}
}
}
Ok(names)
}
fn display_name(full: &[u8], options: &DescribeOptions) -> Option<Vec<u8>> {
let patterned = !options.matches.is_empty() || !options.excludes.is_empty();
match options.candidates {
Candidates::AnnotatedTags | Candidates::Tags => {
let name = full.strip_prefix(b"refs/tags/")?;
options.patterns_allow(name).then(|| name.to_vec())
}
Candidates::All => {
let name = full.strip_prefix(b"refs/")?;
if patterned {
let short = [&b"refs/tags/"[..], b"refs/heads/", b"refs/remotes/"]
.iter()
.find_map(|prefix| full.strip_prefix(*prefix))?;
if !options.patterns_allow(short) {
return None;
}
}
Some(name.to_vec())
}
}
}
fn newest_first(
objects: &Objects,
start: ObjectId,
options: &DescribeOptions,
is_candidate: impl Fn(ObjectId) -> bool,
) -> Result<Vec<(ObjectId, Option<usize>)>, DescribeError> {
let mut found = Vec::new();
let mut counted = 0usize;
let mut seen = IdSet::default();
let mut covered = IdSet::default();
let mut queued = IdSet::default();
let mut uncovered = 0usize;
let mut queue = BinaryHeap::new();
let mut order = 0usize;
seen.insert(start);
queued.insert(start);
uncovered += 1;
queue.push((date(objects, start)?.0, Reverse(order), start, 0usize));
while let Some((_, _, id, steps)) = queue.pop() {
queued.remove(&id);
let mut is_covered = covered.contains(&id);
if !is_covered {
uncovered -= 1;
}
if id != start && is_candidate(id) {
counted += 1;
if !is_covered {
found.push((id, options.first_parent.then_some(steps)));
covered.insert(id);
is_covered = true;
}
if counted == options.max_candidates {
break;
}
}
if !objects.shallow_roots().contains(id) {
let parents = date(objects, id)?.1;
let parents = if options.first_parent {
&parents[..parents.len().min(1)]
} else {
&parents[..]
};
for &parent in parents {
let newly_covered = is_covered && covered.insert(parent);
if seen.insert(parent) {
if seen.len() > options.limits.max_commits {
return Err(HistoryError::Limit("commits").into());
}
order += 1;
queued.insert(parent);
if !covered.contains(&parent) {
uncovered += 1;
}
queue.push((date(objects, parent)?.0, Reverse(order), parent, steps + 1));
} else if newly_covered && queued.contains(&parent) {
uncovered -= 1;
}
}
}
if !found.is_empty() && uncovered == 0 {
break;
}
}
Ok(found)
}
fn date(objects: &Objects, id: ObjectId) -> Result<(i64, Vec<ObjectId>), HistoryError> {
if let Some(record) = objects
.commit_graph
.as_ref()
.and_then(|graph| graph.get(id))
{
let seconds = i64::try_from(record.commit_time).unwrap_or(i64::MAX);
return Ok((seconds, record.parents.clone()));
}
let commit = objects.read_commit(id).map_err(|error| match error {
ReadCommitError::Missing(id) => HistoryError::Missing(id),
ReadCommitError::Kind { id, .. } => HistoryError::NotCommit(id),
ReadCommitError::Parse(source) => HistoryError::Parse { id, source },
ReadCommitError::Read(source) => HistoryError::Read { id, source },
})?;
let seconds = commit
.committer()
.ok()
.flatten()
.and_then(|committer| committer.date().ok())
.map_or(0, |date| date.seconds);
Ok((seconds, commit.parents().to_vec()))
}