1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
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(())
}
}
/// A bit-field which keeps track of which commit is reachable by one of 32 candidates names.
pub 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 {
#[error("The parents of commit {} could not be added to graph during traversal", oid.to_hex())]
InsertParentsToGraph {
#[source]
err: crate::graph::insert_parents::Error,
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};
use bstr::BStr;
use gix_hash::oid;
use super::{Error, Outcome};
use crate::{
describe::{CommitTime, Flags, Options, MAX_CANDIDATES},
Graph, PriorityQueue,
};
/// 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>(
commit: &oid,
graph: &mut Graph<'_, Flags>,
Options {
name_by_oid,
mut max_candidates,
fallback_to_oid,
first_parent,
}: Options<'name>,
) -> Result<Option<Outcome<'name>>, Error> {
let _span = gix_trace::coarse!(
"gix_revision::describe()",
commit = %commit,
name_count = name_by_oid.len(),
max_candidates,
first_parent
);
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 queue = PriorityQueue::from_iter(Some((u32::MAX, commit.to_owned())));
let mut candidates = Vec::new();
let mut commits_seen = 0;
let mut gave_up_on_commit = None;
graph.clear();
graph.insert(commit.to_owned(), 0u32);
while let Some(commit) = queue.pop_value() {
commits_seen += 1;
let flags = 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(),
});
let flags = graph.get_mut(&commit).expect("inserted");
*flags |= identity_bit;
*flags
} else {
gave_up_on_commit = Some(commit);
break;
}
} else {
graph[&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(graph, &mut queue, 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.insert(u32::MAX, commit_id);
commits_seen -= 1;
}
commits_seen += finish_depth_computation(
queue,
graph,
candidates.first_mut().expect("at least one candidate"),
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,
}))
}
fn parents_by_date_onto_queue_and_track_names(
graph: &mut Graph<'_, Flags>,
queue: &mut PriorityQueue<CommitTime, gix_hash::ObjectId>,
commit: gix_hash::ObjectId,
commit_flags: Flags,
first_parent: bool,
) -> Result<(), Error> {
graph
.insert_parents(
&commit,
&mut |parent_id, parent_commit_date| {
queue.insert(parent_commit_date as u32, parent_id);
commit_flags
},
&mut |_parent_id, flags| *flags |= commit_flags,
first_parent,
)
.map_err(|err| Error::InsertParentsToGraph { err, oid: commit })?;
Ok(())
}
fn finish_depth_computation(
mut queue: PriorityQueue<CommitTime, gix_hash::ObjectId>,
graph: &mut Graph<'_, Flags>,
best_candidate: &mut Candidate<'_>,
first_parent: bool,
) -> Result<u32, Error> {
let mut commits_seen = 0;
while let Some(commit) = queue.pop_value() {
commits_seen += 1;
let flags = graph[&commit];
if (flags & best_candidate.identity_bit) == best_candidate.identity_bit {
if queue
.iter_unordered()
.all(|id| (graph[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(graph, &mut queue, 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,
}
}
/// The timestamp for the creation date of a commit in seconds since unix epoch.
type CommitTime = u32;