diff options
Diffstat (limited to 'vendor/gix-revision/src/describe.rs')
-rw-r--r-- | vendor/gix-revision/src/describe.rs | 431 |
1 files changed, 431 insertions, 0 deletions
diff --git a/vendor/gix-revision/src/describe.rs b/vendor/gix-revision/src/describe.rs new file mode 100644 index 000000000..55cc3deef --- /dev/null +++ b/vendor/gix-revision/src/describe.rs @@ -0,0 +1,431 @@ +use std::{ + borrow::Cow, + fmt::{Display, Formatter}, +}; + +use bstr::BStr; +use gix_hashtable::HashMap; + +/// The positive result produced by [describe()][function::describe()]. +#[derive(Debug, Clone)] +pub struct Outcome<'name> { + /// The name of the tag or branch that is closest to the commit `id`. + /// + /// If `None`, no name was found but it was requested to provide the `id` itself as fallback. + pub name: Option<Cow<'name, BStr>>, + /// The input commit object id that we describe. + pub id: gix_hash::ObjectId, + /// The number of commits that are between the tag or branch with `name` and `id`. + /// These commits are all in the future of the named tag or branch. + pub depth: u32, + /// The mapping between object ids and their names initially provided by the describe call. + pub name_by_oid: HashMap<gix_hash::ObjectId, Cow<'name, BStr>>, + /// The amount of commits we traversed. + pub commits_seen: u32, +} + +impl<'a> Outcome<'a> { + /// Turn this outcome into a structure that can display itself in the typical `git describe` format. + pub fn into_format(self, hex_len: usize) -> Format<'a> { + Format { + name: self.name, + id: self.id, + hex_len, + depth: self.depth, + long: false, + dirty_suffix: None, + } + } +} + +/// A structure implementing `Display`, producing a `git describe` like string. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +pub struct Format<'a> { + /// The name of the branch or tag to display, as is. + /// + /// If `None`, the `id` will be displayed as a fallback. + pub name: Option<Cow<'a, BStr>>, + /// The `id` of the commit to describe. + pub id: gix_hash::ObjectId, + /// The amount of hex characters to use to display `id`. + pub hex_len: usize, + /// The amount of commits between `name` and `id`, where `id` is in the future of `name`. + pub depth: u32, + /// If true, the long form of the describe string will be produced even if `id` lies directly on `name`, + /// hence has a depth of 0. + pub long: bool, + /// If `Some(suffix)`, it will be appended to the describe string. + /// This should be set if the working tree was determined to be dirty. + pub dirty_suffix: Option<String>, +} + +impl<'a> Format<'a> { + /// Return true if the `name` is directly associated with `id`, i.e. there are no commits between them. + pub fn is_exact_match(&self) -> bool { + self.depth == 0 + } + + /// Set this instance to print in long mode, that is if `depth` is 0, it will still print the whole + /// long form even though it's not quite necessary. + /// + /// Otherwise, it is allowed to shorten itself. + pub fn long(&mut self, long: bool) -> &mut Self { + self.long = long; + self + } +} + +impl<'a> Display for Format<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if let Some(name) = self.name.as_deref() { + if !self.long && self.is_exact_match() { + name.fmt(f)?; + } else { + write!(f, "{}-{}-g{}", name, self.depth, self.id.to_hex_with_len(self.hex_len))?; + } + } else { + self.id.to_hex_with_len(self.hex_len).fmt(f)?; + } + + if let Some(suffix) = &self.dirty_suffix { + write!(f, "-{suffix}")?; + } + Ok(()) + } +} + +type Flags = u32; +const MAX_CANDIDATES: usize = std::mem::size_of::<Flags>() * 8; + +/// The options required to call [`describe()`][function::describe()]. +#[derive(Clone, Debug)] +pub struct Options<'name> { + /// The candidate names from which to determine the `name` to use for the describe string, + /// as a mapping from a commit id and the name associated with it. + pub name_by_oid: HashMap<gix_hash::ObjectId, Cow<'name, BStr>>, + /// The amount of names we will keep track of. Defaults to the maximum of 32. + /// + /// If the number is exceeded, it will be capped at 32 and defaults to 10. + pub max_candidates: usize, + /// If no candidate for naming, always show the abbreviated hash. Default: false. + pub fallback_to_oid: bool, + /// Only follow the first parent during graph traversal. Default: false. + /// + /// This may speed up the traversal at the cost of accuracy. + pub first_parent: bool, +} + +impl<'name> Default for Options<'name> { + fn default() -> Self { + Options { + max_candidates: 10, // the same number as git uses, otherwise we perform worse by default on big repos + name_by_oid: Default::default(), + fallback_to_oid: false, + first_parent: false, + } + } +} + +/// The error returned by the [`describe()`][function::describe()] function. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error<E> +where + E: std::error::Error + Send + Sync + 'static, +{ + #[error("Commit {} could not be found during graph traversal", .oid.to_hex())] + Find { + #[source] + err: Option<E>, + oid: gix_hash::ObjectId, + }, + #[error("A commit could not be decoded during traversal")] + Decode(#[from] gix_object::decode::Error), +} + +pub(crate) mod function { + use std::{borrow::Cow, cmp::Ordering, collections::VecDeque, iter::FromIterator}; + + use bstr::BStr; + use gix_hash::oid; + use gix_hashtable::{hash_map, HashMap}; + use gix_object::CommitRefIter; + + use super::{Error, Outcome}; + use crate::describe::{Flags, Options, MAX_CANDIDATES}; + + /// Given a `commit` id, traverse the commit graph and collect candidate names from the `name_by_oid` mapping to produce + /// an `Outcome`, which converted [`into_format()`][Outcome::into_format()] will produce a typical `git describe` string. + /// + /// Note that the `name_by_oid` map is returned in the [`Outcome`], which can be forcefully returned even if there was no matching + /// candidate by setting `fallback_to_oid` to true. + pub fn describe<'name, Find, E>( + commit: &oid, + mut find: Find, + Options { + name_by_oid, + mut max_candidates, + fallback_to_oid, + first_parent, + }: Options<'name>, + ) -> Result<Option<Outcome<'name>>, Error<E>> + where + Find: for<'b> FnMut(&oid, &'b mut Vec<u8>) -> Result<Option<CommitRefIter<'b>>, E>, + E: std::error::Error + Send + Sync + 'static, + { + max_candidates = max_candidates.min(MAX_CANDIDATES); + if let Some(name) = name_by_oid.get(commit) { + return Ok(Some(Outcome { + name: name.clone().into(), + id: commit.to_owned(), + depth: 0, + name_by_oid, + commits_seen: 0, + })); + } + + if max_candidates == 0 || name_by_oid.is_empty() { + return if fallback_to_oid { + Ok(Some(Outcome { + id: commit.to_owned(), + name: None, + name_by_oid, + depth: 0, + commits_seen: 0, + })) + } else { + Ok(None) + }; + } + + let mut buf = Vec::new(); + let mut parent_buf = Vec::new(); + + let mut queue = VecDeque::from_iter(Some((commit.to_owned(), u32::MAX))); + let mut candidates = Vec::new(); + let mut commits_seen = 0; + let mut gave_up_on_commit = None; + let mut seen = HashMap::<gix_hash::ObjectId, Flags>::default(); + seen.insert(commit.to_owned(), 0u32); + + while let Some((commit, _commit_time)) = queue.pop_front() { + commits_seen += 1; + if let Some(name) = name_by_oid.get(&commit) { + if candidates.len() < max_candidates { + let identity_bit = 1 << candidates.len(); + candidates.push(Candidate { + name: name.clone(), + commits_in_its_future: commits_seen - 1, + identity_bit, + order: candidates.len(), + }); + *seen.get_mut(&commit).expect("inserted") |= identity_bit; + } else { + gave_up_on_commit = Some(commit); + break; + } + } + + let flags = seen[&commit]; + for candidate in candidates + .iter_mut() + .filter(|c| (flags & c.identity_bit) != c.identity_bit) + { + candidate.commits_in_its_future += 1; + } + + if queue.is_empty() && !candidates.is_empty() { + // single-trunk history that waits to be replenished. + // Abort early if the best-candidate is in the current commits past. + let mut shortest_depth = Flags::MAX; + let mut best_candidates_at_same_depth = 0_u32; + for candidate in &candidates { + match candidate.commits_in_its_future.cmp(&shortest_depth) { + Ordering::Less => { + shortest_depth = candidate.commits_in_its_future; + best_candidates_at_same_depth = candidate.identity_bit; + } + Ordering::Equal => { + best_candidates_at_same_depth |= candidate.identity_bit; + } + Ordering::Greater => {} + } + } + + if (flags & best_candidates_at_same_depth) == best_candidates_at_same_depth { + break; + } + } + + parents_by_date_onto_queue_and_track_names( + &mut find, + &mut buf, + &mut parent_buf, + &mut queue, + &mut seen, + &commit, + flags, + first_parent, + )?; + } + + if candidates.is_empty() { + return if fallback_to_oid { + Ok(Some(Outcome { + id: commit.to_owned(), + name: None, + name_by_oid, + depth: 0, + commits_seen, + })) + } else { + Ok(None) + }; + } + + candidates.sort_by(|a, b| { + a.commits_in_its_future + .cmp(&b.commits_in_its_future) + .then_with(|| a.order.cmp(&b.order)) + }); + + if let Some(commit_id) = gave_up_on_commit { + queue.push_front((commit_id, u32::MAX)); + commits_seen -= 1; + } + + commits_seen += finish_depth_computation( + queue, + find, + candidates.first_mut().expect("at least one candidate"), + seen, + buf, + parent_buf, + first_parent, + )?; + + Ok(candidates.into_iter().next().map(|c| Outcome { + name: c.name.into(), + id: commit.to_owned(), + depth: c.commits_in_its_future, + name_by_oid, + commits_seen, + })) + } + + #[allow(clippy::too_many_arguments)] + fn parents_by_date_onto_queue_and_track_names<Find, E>( + find: &mut Find, + buf: &mut Vec<u8>, + parent_buf: &mut Vec<u8>, + queue: &mut VecDeque<(gix_hash::ObjectId, u32)>, + seen: &mut HashMap<gix_hash::ObjectId, Flags>, + commit: &gix_hash::oid, + commit_flags: Flags, + first_parent: bool, + ) -> Result<(), Error<E>> + where + Find: for<'b> FnMut(&oid, &'b mut Vec<u8>) -> Result<Option<CommitRefIter<'b>>, E>, + E: std::error::Error + Send + Sync + 'static, + { + let commit_iter = find(commit, buf) + .map_err(|err| Error::Find { + err: Some(err), + oid: commit.to_owned(), + })? + .ok_or_else(|| Error::Find { + err: None, + oid: commit.to_owned(), + })?; + for token in commit_iter { + match token { + Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue, + Ok(gix_object::commit::ref_iter::Token::Parent { id: parent_id }) => match seen.entry(parent_id) { + hash_map::Entry::Vacant(entry) => { + let parent = match find(&parent_id, parent_buf).map_err(|err| Error::Find { + err: Some(err), + oid: commit.to_owned(), + })? { + Some(p) => p, + None => continue, // skip missing objects, they don't exist. + }; + + let parent_commit_date = parent + .committer() + .map(|committer| committer.time.seconds_since_unix_epoch) + .unwrap_or_default(); + + entry.insert(commit_flags); + match queue.binary_search_by(|c| c.1.cmp(&parent_commit_date).reverse()) { + Ok(_) => queue.push_back((parent_id, parent_commit_date)), + Err(pos) => queue.insert(pos, (parent_id, parent_commit_date)), + }; + } + hash_map::Entry::Occupied(mut entry) => { + *entry.get_mut() |= commit_flags; + } + }, + Ok(_unused_token) => break, + Err(err) => return Err(err.into()), + } + if first_parent { + break; + } + } + + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn finish_depth_computation<'name, Find, E>( + mut queue: VecDeque<(gix_hash::ObjectId, u32)>, + mut find: Find, + best_candidate: &mut Candidate<'name>, + mut seen: HashMap<gix_hash::ObjectId, Flags>, + mut buf: Vec<u8>, + mut parent_buf: Vec<u8>, + first_parent: bool, + ) -> Result<u32, Error<E>> + where + Find: for<'b> FnMut(&oid, &'b mut Vec<u8>) -> Result<Option<CommitRefIter<'b>>, E>, + E: std::error::Error + Send + Sync + 'static, + { + let mut commits_seen = 0; + while let Some((commit, _commit_time)) = queue.pop_front() { + commits_seen += 1; + let flags = seen[&commit]; + if (flags & best_candidate.identity_bit) == best_candidate.identity_bit { + if queue + .iter() + .all(|(id, _)| (seen[id] & best_candidate.identity_bit) == best_candidate.identity_bit) + { + break; + } + } else { + best_candidate.commits_in_its_future += 1; + } + + parents_by_date_onto_queue_and_track_names( + &mut find, + &mut buf, + &mut parent_buf, + &mut queue, + &mut seen, + &commit, + flags, + first_parent, + )?; + } + Ok(commits_seen) + } + + #[derive(Debug)] + struct Candidate<'a> { + name: Cow<'a, BStr>, + commits_in_its_future: Flags, + /// A single bit identifying this candidate uniquely in a bitset + identity_bit: Flags, + /// The order at which we found the candidate, first one has order = 0 + order: usize, + } +} |