From 29ce60c6729894abeaa230fa7801d259613dd0a1 Mon Sep 17 00:00:00 2001 From: Codex GPT-5 Date: Sun, 21 Jun 2026 14:24:27 +0800 Subject: [PATCH] feat: add commit name-rev support (#1268) Issue #1268 asks for git-name-rev style commit naming so callers can find the branch or ref context for a commit. Git's builtin/name-rev.c propagates prepared ref names backwards through the commit graph, using first-parent generation names like main~2 and side-parent markers like main^2~1. Add gix_revision::name_rev() over prepared tip data, with repository/ref collection and filtering implemented in gix::Commit::name_rev(). The gix layer reuses the describe ref collection path where useful while keeping describe's shortened tag names separate from name-rev's ref-style names. Add a basic gix commit name-rev command with --tags, --refs, --exclude, --always, --no-undefined, and --name-only. Validation: - cargo test -p gix-revision name_rev - cargo test -p gix commit::name_rev - cargo test -p gitoxide-core commit - cargo check -p gix -p gitoxide-core - cargo check -p gix-revision --all-features - cargo check -p gitoxide - cargo run -p gitoxide --bin gix -- commit name-rev --no-undefined --always --exclude=tags/* --exclude=remotes/* HEAD Note: cargo check -p gix -p gix-revision -p gitoxide-core --all-features was attempted, but gix-protocol rejects enabling its mutually exclusive blocking-client and async-client features together. --- gitoxide-core/src/repository/commit.rs | 72 +++ gix-revision/Cargo.toml | 5 +- gix-revision/src/lib.rs | 5 + gix-revision/src/name_rev.rs | 336 ++++++++++++++ gix-revision/tests/revision/main.rs | 2 + gix-revision/tests/revision/name_rev/mod.rs | 179 ++++++++ gix/Cargo.toml | 2 +- gix/src/commit.rs | 433 +++++++++++++++--- gix/src/object/commit.rs | 14 + .../make_commit_name_rev_cutoff.tar | Bin 0 -> 70656 bytes .../make_commit_name_rev_cutoff_sha256.tar | Bin 0 -> 70656 bytes .../make_commit_name_rev_edge_cases.tar | Bin 0 -> 67584 bytes ...make_commit_name_rev_edge_cases_sha256.tar | Bin 0 -> 67584 bytes .../fixtures/make_commit_name_rev_cutoff.sh | 17 + .../make_commit_name_rev_edge_cases.sh | 16 + gix/tests/gix/commit.rs | 116 +++++ src/plumbing/main.rs | 31 ++ src/plumbing/options/mod.rs | 29 ++ 18 files changed, 1188 insertions(+), 69 deletions(-) create mode 100644 gix-revision/src/name_rev.rs create mode 100644 gix-revision/tests/revision/name_rev/mod.rs create mode 100644 gix/tests/fixtures/generated-archives/make_commit_name_rev_cutoff.tar create mode 100644 gix/tests/fixtures/generated-archives/make_commit_name_rev_cutoff_sha256.tar create mode 100644 gix/tests/fixtures/generated-archives/make_commit_name_rev_edge_cases.tar create mode 100644 gix/tests/fixtures/generated-archives/make_commit_name_rev_edge_cases_sha256.tar create mode 100755 gix/tests/fixtures/make_commit_name_rev_cutoff.sh create mode 100755 gix/tests/fixtures/make_commit_name_rev_edge_cases.sh diff --git a/gitoxide-core/src/repository/commit.rs b/gitoxide-core/src/repository/commit.rs index 34a4df2e513..8ee4c491c48 100644 --- a/gitoxide-core/src/repository/commit.rs +++ b/gitoxide-core/src/repository/commit.rs @@ -141,6 +141,66 @@ pub fn describe( Ok(()) } +pub fn name_rev( + repo: gix::Repository, + rev_spec: Option<&str>, + mut out: impl std::io::Write, + name_rev::Options { + tags, + refs, + exclude, + always, + no_undefined, + name_only, + }: name_rev::Options, +) -> Result<()> { + let commit = match rev_spec { + Some(spec) => repo + .rev_parse_single(format!("{spec}^{{commit}}").as_str())? + .object()? + .into_commit(), + None => repo.head_commit()?, + }; + let mut platform = commit + .name_rev() + .names(if tags { + gix::commit::name_rev::SelectRef::AllTags + } else { + gix::commit::name_rev::SelectRef::AllRefs + }) + .id_as_fallback(always); + + for pattern in refs { + platform = platform.include_ref(pattern); + } + for pattern in exclude { + platform = platform.exclude_ref(pattern); + } + + match platform.try_format()? { + Some(format) => { + if name_only { + let mut name = format.to_string(); + if tags { + name = name.strip_prefix("tags/").unwrap_or(&name).to_owned(); + } + writeln!(out, "{name}")?; + } else { + let id_or_spec = rev_spec.map_or_else(|| commit.id.to_string(), ToOwned::to_owned); + writeln!(out, "{id_or_spec} {format}")?; + } + } + None if no_undefined => anyhow::bail!("Cannot describe '{}'", commit.id), + None if name_only => writeln!(out, "undefined")?, + None => { + let id_or_spec = rev_spec.map_or_else(|| commit.id.to_string(), ToOwned::to_owned); + writeln!(out, "{id_or_spec} undefined")?; + } + } + + Ok(()) +} + pub mod describe { #[derive(Debug, Clone)] pub struct Options { @@ -154,3 +214,15 @@ pub mod describe { pub dirty_suffix: Option, } } + +pub mod name_rev { + #[derive(Debug, Clone)] + pub struct Options { + pub tags: bool, + pub refs: Vec, + pub exclude: Vec, + pub always: bool, + pub no_undefined: bool, + pub name_only: bool, + } +} diff --git a/gix-revision/Cargo.toml b/gix-revision/Cargo.toml index e511638c1f3..229f2821d8d 100644 --- a/gix-revision/Cargo.toml +++ b/gix-revision/Cargo.toml @@ -15,7 +15,7 @@ rust-version = "1.85" doctest = false [features] -default = ["describe", "merge_base"] +default = ["describe", "merge_base", "name_rev"] ## Enable support for the SHA-1 hash by enabling the respective feature in the `gix-hash` crate. sha1 = ["gix-hash/sha1"] ## Enable support for the SHA-256 hash by enabling the respective feature in the `gix-hash` crate. @@ -24,6 +24,9 @@ sha256 = ["gix-hash/sha256"] ## `git describe` functionality describe = ["dep:gix-trace", "dep:gix-hashtable"] +## `git name-rev` functionality +name_rev = ["dep:gix-trace"] + ## `git merge-base` functionality merge_base = ["dep:gix-trace", "dep:bitflags"] diff --git a/gix-revision/src/lib.rs b/gix-revision/src/lib.rs index bd2bfab94de..67b3d242da6 100644 --- a/gix-revision/src/lib.rs +++ b/gix-revision/src/lib.rs @@ -14,6 +14,11 @@ pub mod describe; #[cfg(feature = "describe")] pub use describe::function::describe; /// +#[cfg(feature = "name_rev")] +pub mod name_rev; +#[cfg(feature = "name_rev")] +pub use name_rev::function::name_rev; +/// #[allow(clippy::empty_docs)] #[cfg(feature = "merge_base")] pub mod merge_base; diff --git a/gix-revision/src/name_rev.rs b/gix-revision/src/name_rev.rs new file mode 100644 index 00000000000..42039bc9893 --- /dev/null +++ b/gix-revision/src/name_rev.rs @@ -0,0 +1,336 @@ +use std::{ + borrow::Cow, + fmt::{Display, Formatter}, +}; + +use bstr::{BStr, BString, ByteVec}; + +/// A prepared reference tip from which names will be propagated through the commit graph. +#[derive(Debug, Clone)] +pub struct Tip<'name> { + /// The peeled commit id the name points to. + pub id: gix_hash::ObjectId, + /// The display name associated with `id`. + pub name: Cow<'name, BStr>, + /// The timestamp used for ordering competing names. + pub taggerdate: i64, + /// If true, this tip came from a tag reference. + pub from_tag: bool, + /// If true, the display name names a tag object and needs `^0` when naming the tagged commit directly. + pub deref: bool, +} + +/// The positive result produced by [name_rev()][function::name_rev()]. +#[derive(Debug, Clone)] +pub struct Outcome<'name> { + /// The input commit object id that we name. + pub id: gix_hash::ObjectId, + /// The name found for `id`, or `None` if fallback formatting was requested and no name exists. + pub name: Option>, + /// The amount of commits traversed while propagating names. + pub commits_seen: u32, +} + +impl<'name> Outcome<'name> { + /// Turn this outcome into a displayable `git name-rev`-like format. + pub fn into_format(self, hex_len: usize) -> Format<'name> { + Format { + id: self.id, + name: self.name, + hex_len, + } + } +} + +/// A structure implementing `Display`, producing a `git name-rev`-like string. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +pub struct Format<'name> { + /// The object id to use if no symbolic name is available. + pub id: gix_hash::ObjectId, + /// The symbolic name to display, if one was found. + pub name: Option>, + /// The amount of hex characters to use for id fallback formatting. + pub hex_len: usize, +} + +impl Display for Format<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self.name.as_deref() { + Some(name) => name.fmt(f), + None => self.id.to_hex_with_len(self.hex_len).fmt(f), + } + } +} + +/// The options required to call [`name_rev()`][function::name_rev()]. +#[derive(Clone, Debug, Default)] +pub struct Options<'name> { + /// The prepared tips from which to propagate names through history. + pub tips: Vec>, + /// If no candidate for naming exists, always show the abbreviated hash. + pub fallback_to_oid: bool, +} + +/// The error returned by the [`name_rev()`](function::name_rev()) function. +pub type Error = gix_error::Message; + +/// The per-commit state stored in the graph while running [`name_rev()`](function::name_rev()). +#[derive(Debug, Clone)] +pub struct State<'name> { + tip_name: Cow<'name, BStr>, + taggerdate: i64, + generation: u32, + distance: u32, + from_tag: bool, +} + +impl State<'_> { + fn format(&self) -> BString { + if self.generation == 0 { + self.tip_name.as_ref().to_owned() + } else { + let base = self.tip_name.as_ref().strip_suffix(b"^0").unwrap_or(&self.tip_name); + let mut out = BString::from(base); + out.push_str(format!("~{}", self.generation)); + out + } + } +} + +pub(crate) mod function { + use std::{borrow::Cow, cmp::Ordering}; + + use bstr::{BString, ByteVec}; + use gix_error::{Exn, ResultExt, message}; + use gix_hash::oid; + + use super::{Error, Outcome, State}; + use crate::{Graph, name_rev::Options}; + + const MERGE_TRAVERSAL_WEIGHT: u32 = 65535; + const CUTOFF_DATE_SLOP: i64 = 86400; + + /// Given a `commit` id, propagate all prepared names through `graph` and produce an `Outcome`. + pub fn name_rev<'name>( + commit: &oid, + graph: &mut Graph<'_, '_, State<'name>>, + Options { + mut tips, + fallback_to_oid, + }: Options<'name>, + ) -> Result>, Exn> { + let _span = gix_trace::coarse!( + "gix_revision::name_rev()", + commit = %commit, + name_count = tips.len() + ); + + if tips.is_empty() { + return if fallback_to_oid { + Ok(Some(Outcome { + id: commit.to_owned(), + name: None, + commits_seen: 0, + })) + } else { + Ok(None) + }; + } + + tips.sort_by(|a, b| { + b.from_tag + .cmp(&a.from_tag) + .then_with(|| a.taggerdate.cmp(&b.taggerdate)) + }); + + let mut commits_seen = 0; + graph.clear(); + let cutoff = cutoff_timestamp( + graph + .lookup(commit) + .or_raise(|| message!("could not lookup commit {}", commit.to_hex()))? + .committer_timestamp() + .or_raise(|| message!("could not read commit timestamp {}", commit.to_hex()))?, + ); + + for tip in tips { + if graph + .lookup(&tip.id) + .or_raise(|| message!("could not lookup tip commit {}", tip.id.to_hex()))? + .committer_timestamp() + .or_raise(|| message!("could not read tip commit timestamp {}", tip.id.to_hex()))? + < cutoff + { + continue; + } + + let mut name = tip.name.into_owned(); + if tip.deref { + name.push_str("^0"); + } + let name = State { + tip_name: Cow::Owned(name), + taggerdate: tip.taggerdate, + generation: 0, + distance: 0, + from_tag: tip.from_tag, + }; + + if create_or_update_name(graph, tip.id, name) { + propagate_names(tip.id, cutoff, graph, &mut commits_seen)?; + } + } + + match graph.get(commit) { + Some(name) => Ok(Some(Outcome { + id: commit.to_owned(), + name: Some(Cow::Owned(name.format())), + commits_seen, + })), + None if fallback_to_oid => Ok(Some(Outcome { + id: commit.to_owned(), + name: None, + commits_seen, + })), + None => Ok(None), + } + } + + fn propagate_names( + start: gix_hash::ObjectId, + cutoff: i64, + graph: &mut Graph<'_, '_, State<'_>>, + commits_seen: &mut u32, + ) -> Result<(), Exn> { + let mut stack = vec![start]; + let mut parents_to_queue = Vec::new(); + + while let Some(commit) = stack.pop() { + *commits_seen += 1; + let name = graph.get(&commit).expect("all queued commits have names").clone(); + + parents_to_queue.clear(); + let parents = { + let commit_object = graph + .lookup(&commit) + .or_raise(|| message!("could not lookup commit {}", commit.to_hex()))?; + commit_object + .iter_parents() + .collect::, _>>() + .or_raise(|| message!("could not read parents of commit {}", commit.to_hex()))? + }; + + for (parent_index, parent_id) in parents.into_iter().enumerate() { + let Some(parent_commit) = graph + .try_lookup(&parent_id) + .or_raise(|| message!("could not lookup parent commit {}", parent_id.to_hex()))? + else { + continue; + }; + if parent_commit + .committer_timestamp() + .or_raise(|| message!("could not read parent commit timestamp {}", parent_id.to_hex()))? + < cutoff + { + continue; + } + + let parent_number = parent_index as u32 + 1; + let (tip_name, generation, distance) = if parent_number > 1 { + ( + Cow::Owned(parent_name(&name, parent_number)), + 0, + name.distance.saturating_add(MERGE_TRAVERSAL_WEIGHT), + ) + } else { + ( + name.tip_name.clone(), + name.generation.saturating_add(1), + name.distance.saturating_add(1), + ) + }; + + let parent_name = State { + tip_name, + taggerdate: name.taggerdate, + generation, + distance, + from_tag: name.from_tag, + }; + + if create_or_update_name(graph, parent_id, parent_name) { + parents_to_queue.push(parent_id); + } + } + + while let Some(parent) = parents_to_queue.pop() { + stack.push(parent); + } + } + + Ok(()) + } + + fn cutoff_timestamp(timestamp: i64) -> i64 { + timestamp.saturating_sub(CUTOFF_DATE_SLOP) + } + + fn create_or_update_name<'name>( + graph: &mut Graph<'_, '_, State<'name>>, + id: gix_hash::ObjectId, + new_name: State<'name>, + ) -> bool { + match graph.get_mut(&id) { + Some(name) => { + if !is_better_name(name, &new_name) { + return false; + } + *name = new_name; + } + None => { + graph.insert(id, new_name); + } + } + true + } + + fn is_better_name(current: &State<'_>, new: &State<'_>) -> bool { + let current_distance = effective_distance(current.distance, current.generation); + let new_distance = effective_distance(new.distance, new.generation); + + if new.from_tag && current.from_tag { + return current_distance > new_distance; + } + + if current.from_tag != new.from_tag { + return new.from_tag; + } + + match current_distance.cmp(&new_distance) { + Ordering::Greater => return true, + Ordering::Less => return false, + Ordering::Equal => {} + } + + if current.taggerdate != new.taggerdate { + return current.taggerdate > new.taggerdate; + } + + false + } + + fn effective_distance(distance: u32, generation: u32) -> u32 { + distance.saturating_add(if generation > 0 { MERGE_TRAVERSAL_WEIGHT } else { 0 }) + } + + fn parent_name(name: &State<'_>, parent_number: u32) -> BString { + let base = name.tip_name.as_ref().strip_suffix(b"^0").unwrap_or(&name.tip_name); + let mut out = BString::from(base); + if name.generation > 0 { + out.push_str(format!("~{}^{}", name.generation, parent_number)); + } else { + out.push_str(format!("^{parent_number}")); + } + out + } +} diff --git a/gix-revision/tests/revision/main.rs b/gix-revision/tests/revision/main.rs index 9d2e9b77041..b80e370ae89 100644 --- a/gix-revision/tests/revision/main.rs +++ b/gix-revision/tests/revision/main.rs @@ -2,6 +2,8 @@ mod describe; #[cfg(feature = "merge_base")] mod merge_base; +#[cfg(feature = "name_rev")] +mod name_rev; mod spec; pub use gix_testtools::Result; diff --git a/gix-revision/tests/revision/name_rev/mod.rs b/gix-revision/tests/revision/name_rev/mod.rs new file mode 100644 index 00000000000..bfce397bd9e --- /dev/null +++ b/gix-revision/tests/revision/name_rev/mod.rs @@ -0,0 +1,179 @@ +use bstr::ByteSlice; +use gix_error::Exn; +use gix_revision::{ + name_rev, + name_rev::{Error, Outcome, Tip}, +}; +use std::{borrow::Cow, path::PathBuf}; + +use crate::hex_to_id; + +fn run_test( + options: impl Fn(gix_hash::ObjectId) -> gix_revision::name_rev::Options<'static>, + run_assertions: impl Fn( + Result>, Exn>, + gix_hash::ObjectId, + ) -> Result<(), gix_error::Error>, +) -> Result<(), gix_error::Error> { + let store = odb_at("."); + let commit_id = hex_to_id("01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"); + for use_commitgraph in [false, true] { + let cache = use_commitgraph + .then(|| gix_commitgraph::Graph::from_info_dir(&store.store_ref().path().join("info")).ok()) + .flatten(); + let mut graph = gix_revision::Graph::new(&store, cache.as_ref()); + run_assertions( + gix_revision::name_rev(&commit_id, &mut graph, options(commit_id)), + commit_id, + )?; + } + Ok(()) +} + +#[test] +fn direct_tip_match() -> Result<(), gix_error::Error> { + run_test( + |id| name_rev::Options { + tips: vec![tip(id, "main", false)], + ..Default::default() + }, + |res, id| { + let res = res?.expect("name found"); + assert_eq!(res.id, id); + assert_eq!(res.name.as_deref(), Some("main".as_bytes().as_bstr())); + assert_eq!(res.into_format(7).to_string(), "main"); + Ok(()) + }, + ) +} + +#[test] +fn first_parent_ancestor_is_named_by_generation() -> Result<(), gix_error::Error> { + run_test( + |id| name_rev::Options { + tips: vec![tip(id, "main", false)], + ..Default::default() + }, + |_res, _id| { + let store = odb_at("."); + let mut graph = gix_revision::Graph::new(&store, None); + let res = gix_revision::name_rev( + &hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"), + &mut graph, + name_rev::Options { + tips: vec![tip( + hex_to_id("01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"), + "main", + false, + )], + ..Default::default() + }, + )? + .expect("name found"); + assert_eq!(res.into_format(7).to_string(), "main~1"); + Ok(()) + }, + ) +} + +#[test] +fn side_parent_paths_are_preserved() -> Result<(), gix_error::Error> { + run_test( + |id| name_rev::Options { + tips: vec![tip(id, "main", false)], + ..Default::default() + }, + |_res, _id| { + let store = odb_at("."); + let mut graph = gix_revision::Graph::new(&store, None); + let res = gix_revision::name_rev( + &hex_to_id("9152eeee2328073cf23dcf8e90c949170b711659"), + &mut graph, + name_rev::Options { + tips: vec![tip( + hex_to_id("01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"), + "main", + false, + )], + ..Default::default() + }, + )? + .expect("name found"); + assert_eq!(res.into_format(7).to_string(), "main^2~1"); + Ok(()) + }, + ) +} + +#[test] +fn tags_are_preferred_over_branches() -> Result<(), gix_error::Error> { + run_test( + |id| name_rev::Options { + tips: vec![ + tip(id, "main", false), + tip( + hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"), + "tags/at-c5", + true, + ), + ], + ..Default::default() + }, + |_res, _id| { + let store = odb_at("."); + let mut graph = gix_revision::Graph::new(&store, None); + let res = gix_revision::name_rev( + &hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"), + &mut graph, + name_rev::Options { + tips: vec![ + tip(hex_to_id("01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"), "main", false), + tip( + hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"), + "tags/at-c5", + true, + ), + ], + ..Default::default() + }, + )? + .expect("name found"); + assert_eq!(res.into_format(7).to_string(), "tags/at-c5"); + Ok(()) + }, + ) +} + +#[test] +fn fallback_if_configured_but_no_name_matches() -> Result<(), gix_error::Error> { + run_test( + |_| name_rev::Options { + fallback_to_oid: true, + ..Default::default() + }, + |res, _id| { + let res = res?.expect("fallback active"); + assert!(res.name.is_none(), "no symbolic name was found"); + assert_eq!(res.into_format(7).to_string(), "01ec18a"); + Ok(()) + }, + ) +} + +fn tip(id: gix_hash::ObjectId, name: &'static str, from_tag: bool) -> Tip<'static> { + Tip { + id, + name: Cow::Borrowed(name.as_bytes().as_bstr()), + taggerdate: 0, + from_tag, + deref: false, + } +} + +fn odb_at(name: &str) -> gix_odb::Handle { + gix_odb::at(fixture_path().join(name).join(".git/objects")).unwrap() +} + +fn fixture_path() -> PathBuf { + gix_testtools::scripted_fixture_read_only("make_repo_with_branches.sh").unwrap() +} diff --git a/gix/Cargo.toml b/gix/Cargo.toml index c4d95eb9978..4465e0ef19d 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -153,7 +153,7 @@ attributes = [ mailmap = ["dep:gix-mailmap", "revision"] ## Make revspec parsing possible, as well describing revision. -revision = ["gix-revision/describe", "gix-revision/merge_base", "index"] +revision = ["gix-revision/describe", "gix-revision/merge_base", "gix-revision/name_rev", "index"] ## If enabled, revspecs now support the regex syntax like `@^{/^.*x}`. Otherwise, only substring search is supported. ## This feature does increase compile time for niche-benefit, but is required for fully git-compatible revspec parsing. diff --git a/gix/src/commit.rs b/gix/src/commit.rs index 835cb0bfa83..9dbfddd8d72 100644 --- a/gix/src/commit.rs +++ b/gix/src/commit.rs @@ -30,6 +30,207 @@ impl From for Error { } } +#[cfg(feature = "revision")] +mod named_refs { + use std::borrow::Cow; + + use crate::bstr::{BStr, BString, ByteSlice}; + use gix_hash::ObjectId; + + use crate::Repository; + + /// A selector to choose what kind of references should contribute to names. + #[derive(Default, Debug, Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)] + pub enum SelectRef { + /// Only use annotated tags for names. + #[default] + AnnotatedTags, + /// Use all tags for names, annotated or plain reference. + AllTags, + /// Use all references, including local branch names. + AllRefs, + } + + pub(crate) struct Candidate { + pub(crate) peeled_id: ObjectId, + pub(crate) name: Cow<'static, BStr>, + pub(crate) name_rev_name: Cow<'static, BStr>, + pub(crate) priority: u8, + pub(crate) taggerdate: i64, + pub(crate) from_tag: bool, + pub(crate) deref: bool, + } + + pub(crate) struct Filters<'a> { + pub(crate) include: &'a [BString], + pub(crate) exclude: &'a [BString], + } + + impl Filters<'_> { + pub(crate) const NONE: Filters<'static> = Filters { + include: &[], + exclude: &[], + }; + + fn allows(&self, name: &BStr) -> Option { + if self + .exclude + .iter() + .any(|pattern| subpath_match(name, pattern.as_bstr()).is_some()) + { + return None; + } + + if self.include.is_empty() { + return Some(false); + } + + let mut matched = false; + let mut abbreviate = false; + for pattern in self.include { + if let Some(is_subpath_match) = subpath_match(name, pattern.as_bstr()) { + matched = true; + abbreviate |= is_subpath_match; + } + } + matched.then_some(abbreviate) + } + } + + pub(crate) fn collect(repo: &Repository, select: SelectRef, filters: Filters<'_>) -> Result, E> + where + E: From + From, + { + let platform = repo.references()?; + let refs = match select { + SelectRef::AllRefs => platform.all()?, + SelectRef::AllTags | SelectRef::AnnotatedTags => platform.tags()?, + }; + + let mut out = Vec::new(); + for reference in refs.filter_map(Result::ok) { + let Some(abbreviate_name_rev) = filters.allows(reference.inner.name.as_bstr()) else { + continue; + }; + + match select { + SelectRef::AnnotatedTags => { + if let Some(candidate) = annotated_tag_candidate(repo, reference, abbreviate_name_rev) { + out.push(candidate); + } + } + SelectRef::AllTags | SelectRef::AllRefs => { + if let Some(candidate) = any_ref_candidate(repo, reference, abbreviate_name_rev) { + out.push(candidate); + } + } + } + } + + Ok(out) + } + + fn any_ref_candidate( + repo: &Repository, + mut reference: crate::Reference<'_>, + abbreviate_name_rev: bool, + ) -> Option { + let full_name = reference.inner.name.as_bstr().to_owned(); + let name = Cow::from(reference.inner.name.shorten().to_owned()); + let name_rev_name = if abbreviate_name_rev { + name.clone() + } else { + Cow::from(name_rev_name(full_name.as_bstr())) + }; + let target_id = reference.target().try_id().map(ToOwned::to_owned); + let peeled_id = reference.peel_to_id().ok()?; + let from_tag = full_name.starts_with_str("refs/tags/"); + let mut priority = 0; + let mut taggerdate = 0; + let deref = match target_id { + Some(target_id) if peeled_id != *target_id => { + let tag = repo.find_object(target_id).ok()?.try_into_tag().ok()?; + taggerdate = tag.tagger().ok().and_then(|s| s.map(|s| s.seconds())).unwrap_or(0); + priority = 1; + true + } + _ => false, + }; + + Some(Candidate { + peeled_id: peeled_id.inner, + name, + name_rev_name, + priority, + taggerdate, + from_tag, + deref, + }) + } + + fn annotated_tag_candidate( + _repo: &Repository, + reference: crate::Reference<'_>, + abbreviate_name_rev: bool, + ) -> Option { + // TODO: we assume direct refs for tags, which is the common case, but it doesn't have to be + // so rather follow symrefs till the first object and then peel tags after the first object was found. + let name = Cow::from(reference.name().shorten().to_owned()); + let name_rev_name = if abbreviate_name_rev { + name.clone() + } else { + Cow::from(name_rev_name(reference.name().as_bstr())) + }; + let tag = reference.try_id()?.object().ok()?.try_into_tag().ok()?; + let taggerdate = tag.tagger().ok().and_then(|s| s.map(|s| s.seconds())).unwrap_or(0); + let commit_id = tag.target_id().ok()?.object().ok()?.try_into_commit().ok()?.id; + Some(Candidate { + peeled_id: commit_id, + name, + name_rev_name, + priority: 1, + taggerdate, + from_tag: true, + deref: true, + }) + } + + pub(crate) fn commit_time(repo: &Repository, id: ObjectId) -> Option { + repo.find_object(id) + .ok()? + .try_into_commit() + .ok()? + .committer() + .ok() + .map(|committer| committer.seconds()) + } + + fn name_rev_name(name: &BStr) -> BString { + name.strip_prefix(b"refs/heads/") + .or_else(|| name.strip_prefix(b"refs/")) + .unwrap_or(name) + .to_owned() + .into() + } + + fn subpath_match(name: &BStr, pattern: &BStr) -> Option { + let mut subpath = name; + let mut is_subpath = false; + loop { + if gix_glob::wildmatch(pattern, subpath, gix_glob::wildmatch::Mode::empty()) { + return Some(is_subpath); + } + match subpath.find_byte(b'/') { + Some(pos) => { + subpath = subpath[pos + 1..].as_bstr(); + is_subpath = true; + } + None => return None, + } + } + } +} + /// #[cfg(feature = "revision")] pub mod describe { @@ -40,6 +241,8 @@ pub mod describe { use crate::{Repository, bstr::BStr, ext::ObjectIdExt}; + pub use super::named_refs::SelectRef; + /// The result of [`try_resolve()`][Platform::try_resolve()]. pub struct Resolution<'repo> { /// The outcome of the describe operation. @@ -96,83 +299,30 @@ pub mod describe { DetermineIsDirty(#[from] crate::status::is_dirty::Error), } - /// A selector to choose what kind of references should contribute to names. - #[derive(Default, Debug, Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)] - pub enum SelectRef { - /// Only use annotated tags for names. - #[default] - AnnotatedTags, - /// Use all tags for names, annotated or plain reference. - AllTags, - /// Use all references, including local branch names. - AllRefs, - } - impl SelectRef { fn names(&self, repo: &Repository) -> Result>, Error> { - let platform = repo.references()?; - Ok(match self { SelectRef::AllTags | SelectRef::AllRefs => { - let mut refs: Vec<_> = match self { - SelectRef::AllRefs => platform.all()?, - SelectRef::AllTags => platform.tags()?, - _ => unreachable!(), - } - .filter_map(Result::ok) - .filter_map(|mut r: crate::Reference<'_>| { - let target_id = r.target().try_id().map(ToOwned::to_owned); - let peeled_id = r.peel_to_id().ok()?; - let (prio, tag_time) = match target_id { - Some(target_id) if peeled_id != *target_id => { - let tag = repo.find_object(target_id).ok()?.try_into_tag().ok()?; - let tag_time = tag.tagger().ok().and_then(|s| s.map(|s| s.seconds())).unwrap_or(0); - (1, tag_time) - } - _ => (0, 0), - }; - ( - peeled_id.inner, - prio, - tag_time, - Cow::from(r.inner.name.shorten().to_owned()), - ) - .into() - }) - .collect(); + let mut refs = super::named_refs::collect::(repo, *self, super::named_refs::Filters::NONE)?; // By priority, then by time ascending, then lexicographically. // More recent entries overwrite older ones due to collection into hashmap. - refs.sort_by( - |(_a_peeled_id, a_prio, a_time, a_name), (_b_peeled_id, b_prio, b_time, b_name)| { - a_prio - .cmp(b_prio) - .then_with(|| a_time.cmp(b_time)) - .then_with(|| b_name.cmp(a_name)) - }, - ); - refs.into_iter().map(|(a, _, _, b)| (a, b)).collect() + refs.sort_by(|a, b| { + a.priority + .cmp(&b.priority) + .then_with(|| a.taggerdate.cmp(&b.taggerdate)) + .then_with(|| b.name.cmp(&a.name)) + }); + refs.into_iter() + .map(|candidate| (candidate.peeled_id, candidate.name)) + .collect() } SelectRef::AnnotatedTags => { - let mut peeled_commits_and_tag_date: Vec<_> = platform - .tags()? - .filter_map(Result::ok) - .filter_map(|r: crate::Reference<'_>| { - // TODO: we assume direct refs for tags, which is the common case, but it doesn't have to be - // so rather follow symrefs till the first object and then peel tags after the first object was found. - let tag = r.try_id()?.object().ok()?.try_into_tag().ok()?; - let tag_time = tag.tagger().ok().and_then(|s| s.map(|s| s.seconds())).unwrap_or(0); - let commit_id = tag.target_id().ok()?.object().ok()?.try_into_commit().ok()?.id; - Some((commit_id, tag_time, Cow::::from(r.name().shorten().to_owned()))) - }) - .collect(); + let mut refs = super::named_refs::collect::(repo, *self, super::named_refs::Filters::NONE)?; // Sort by time ascending, then lexicographically. // More recent entries overwrite older ones due to collection into hashmap. - peeled_commits_and_tag_date.sort_by(|(_a_id, a_time, a_name), (_b_id, b_time, b_name)| { - a_time.cmp(b_time).then_with(|| b_name.cmp(a_name)) - }); - peeled_commits_and_tag_date - .into_iter() - .map(|(a, _, c)| (a, c)) + refs.sort_by(|a, b| a.taggerdate.cmp(&b.taggerdate).then_with(|| b.name.cmp(&a.name))); + refs.into_iter() + .map(|candidate| (candidate.peeled_id, candidate.name)) .collect() } }) @@ -270,3 +420,152 @@ pub mod describe { } } } + +/// +#[cfg(feature = "revision")] +pub mod name_rev { + use crate::bstr::BString; + use gix_error::Exn; + + use crate::ext::ObjectIdExt; + + pub use super::named_refs::SelectRef; + + /// The result of [`try_resolve()`][Platform::try_resolve()]. + pub struct Resolution<'repo> { + /// The outcome of the name-rev operation. + pub outcome: gix_revision::name_rev::Outcome<'static>, + /// The id to name. + pub id: crate::Id<'repo>, + } + + impl Resolution<'_> { + /// Turn this instance into something displayable. + pub fn format(self) -> Result, Error> { + let prefix = self.id.shorten()?; + Ok(self.outcome.into_format(prefix.hex_len())) + } + } + + /// The error returned by [`try_format()`][Platform::try_format()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + OpenCache(#[from] crate::repository::commit_graph_if_enabled::Error), + #[error(transparent)] + NameRev(#[from] gix_revision::name_rev::Error), + #[error("Could not produce an unambiguous shortened id for formatting.")] + ShortId(#[from] crate::id::shorten::Error), + #[error(transparent)] + RefIter(#[from] crate::reference::iter::Error), + #[error(transparent)] + RefIterInit(#[from] crate::reference::iter::init::Error), + } + + /// A support type to allow configuring a `git name-rev` operation. + pub struct Platform<'repo> { + pub(crate) id: gix_hash::ObjectId, + /// The owning repository. + pub repo: &'repo crate::Repository, + pub(crate) select: SelectRef, + pub(crate) include_refs: Vec, + pub(crate) exclude_refs: Vec, + pub(crate) id_as_fallback: bool, + } + + impl<'repo> Platform<'repo> { + /// Configure which references to use for names. + pub fn names(mut self, select: SelectRef) -> Self { + self.select = select; + self + } + + /// Only use references matching `pattern`. + pub fn include_ref(mut self, pattern: impl Into) -> Self { + self.include_refs.push(pattern.into()); + self + } + + /// Ignore references matching `pattern`. + pub fn exclude_ref(mut self, pattern: impl Into) -> Self { + self.exclude_refs.push(pattern.into()); + self + } + + /// If true, even if no name is available a format will always be produced. + pub fn id_as_fallback(mut self, use_fallback: bool) -> Self { + self.id_as_fallback = use_fallback; + self + } + + /// Try to find a name for the configured commit id using all prior configuration. + pub fn try_format(&self) -> Result>, Error> { + self.try_resolve()?.map(Resolution::format).transpose() + } + + /// Try to find a name for the configured commit id using all prior configuration, returning `Some(Outcome)` + /// if one was found. + /// + /// # Performance + /// + /// Prefer to use [`Self::try_resolve_with_cache()`] when processing more than one commit at a time. + pub fn try_resolve_with_cache( + &self, + cache: Option<&'_ gix_commitgraph::Graph>, + ) -> Result>, Error> { + let mut graph = self.repo.revision_graph(cache); + let tips = super::named_refs::collect::( + self.repo, + self.select, + super::named_refs::Filters { + include: &self.include_refs, + exclude: &self.exclude_refs, + }, + )? + .into_iter() + .filter_map(|candidate| { + let taggerdate = if candidate.deref { + candidate.taggerdate + } else { + super::named_refs::commit_time(self.repo, candidate.peeled_id)? + }; + Some(gix_revision::name_rev::Tip { + id: candidate.peeled_id, + name: candidate.name_rev_name, + taggerdate, + from_tag: candidate.from_tag, + deref: candidate.deref, + }) + }) + .collect(); + + let outcome = gix_revision::name_rev( + &self.id, + &mut graph, + gix_revision::name_rev::Options { + tips, + fallback_to_oid: self.id_as_fallback, + }, + ) + .map_err(Exn::into_inner)?; + + Ok(outcome.map(|outcome| Resolution { + outcome, + id: self.id.attach(self.repo), + })) + } + + /// Like [`Self::try_resolve_with_cache()`], but obtains the commitgraph-cache internally for a single use. + pub fn try_resolve(&self) -> Result>, Error> { + let cache = self.repo.commit_graph_if_enabled()?; + self.try_resolve_with_cache(cache.as_ref()) + } + + /// Like [`try_format()`](Self::try_format()), but turns `id_as_fallback()` on to always produce a format. + pub fn format(&mut self) -> Result, Error> { + self.id_as_fallback = true; + Ok(self.try_format()?.expect("BUG: fallback must always produce a format")) + } + } +} diff --git a/gix/src/object/commit.rs b/gix/src/object/commit.rs index 988a5c9082d..67cdc3434e0 100644 --- a/gix/src/object/commit.rs +++ b/gix/src/object/commit.rs @@ -211,6 +211,20 @@ impl<'repo> Commit<'repo> { } } + /// Create a platform to further configure a `git name-rev` operation to find a name for this commit by looking + /// at all references by default. + #[cfg(feature = "revision")] + pub fn name_rev(&self) -> crate::commit::name_rev::Platform<'repo> { + crate::commit::name_rev::Platform { + id: self.id, + repo: self.repo, + select: crate::commit::name_rev::SelectRef::AllRefs, + include_refs: Vec::new(), + exclude_refs: Vec::new(), + id_as_fallback: false, + } + } + /// Extracts the PGP signature and the data that was used to create the signature, or `None` if it wasn't signed. // TODO: make it possible to verify the signature, probably by wrapping `SignedData`. It's quite some work to do it properly. pub fn signature( diff --git a/gix/tests/fixtures/generated-archives/make_commit_name_rev_cutoff.tar b/gix/tests/fixtures/generated-archives/make_commit_name_rev_cutoff.tar new file mode 100644 index 0000000000000000000000000000000000000000..68758a0796af3df56c0c1a8ff52996fa06592377 GIT binary patch literal 70656 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 literal 0 HcmV?d00001 diff --git a/gix/tests/fixtures/generated-archives/make_commit_name_rev_cutoff_sha256.tar b/gix/tests/fixtures/generated-archives/make_commit_name_rev_cutoff_sha256.tar new file mode 100644 index 0000000000000000000000000000000000000000..f76f84d695294e9d312c984aad362ffd2f601317 GIT binary patch literal 70656 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 G2L3 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 vOyO&7yOsu+R-vQ(yj)yk|K?=>qy$!*RNmC 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 +git add f +GIT_AUTHOR_DATE="2001-01-01T00:00:00+0000" GIT_COMMITTER_DATE="2001-01-01T00:00:00+0000" git commit -qm base +git branch skew-target + +echo skewed-child >>f +git add f +GIT_AUTHOR_DATE="2000-01-01T00:00:00+0000" GIT_COMMITTER_DATE="2000-01-01T00:00:00+0000" git commit -qm skewed-child +GIT_COMMITTER_DATE="2000-02-01T00:00:00+0000" git tag -a skewed-tag -m skewed-tag diff --git a/gix/tests/fixtures/make_commit_name_rev_edge_cases.sh b/gix/tests/fixtures/make_commit_name_rev_edge_cases.sh new file mode 100755 index 00000000000..8a1f36b248b --- /dev/null +++ b/gix/tests/fixtures/make_commit_name_rev_edge_cases.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init -q + +# One commit with far-future annotated tags checks that ref sorting keeps the full +# Git timestamp range instead of truncating to u32. +echo A >f +git add f +GIT_COMMITTER_DATE="@1 +0000" GIT_AUTHOR_DATE="@1 +0000" git commit -qm A +GIT_COMMITTER_DATE="@4294967296 +0000" git tag -a a -m a +GIT_COMMITTER_DATE="@4294967297 +0000" git tag -a z -m z + +# A lightweight tag to a blob must be ignored by name-rev even when all tags are +# selected. Passing it to graph traversal would try to look up the blob as a commit. +git tag blob-tag HEAD:f diff --git a/gix/tests/gix/commit.rs b/gix/tests/gix/commit.rs index 2d05ef95096..d76e8c58cf6 100644 --- a/gix/tests/gix/commit.rs +++ b/gix/tests/gix/commit.rs @@ -86,4 +86,120 @@ mod describe { } Ok(()) } + + #[test] + fn tags_preserve_the_full_git_timestamp_range() -> crate::Result { + let repo = named_repo("make_commit_name_rev_edge_cases.sh")?; + let mut describe = repo.head_commit()?.describe(); + for filter in &[AnnotatedTags, AllTags, AllRefs] { + describe = describe.names(*filter); + assert_eq!(describe.format()?.to_string(), "z", "{filter:?}"); + } + Ok(()) + } +} + +#[cfg(feature = "revision")] +mod name_rev { + use gix::commit::name_rev::SelectRef::{AllRefs, AllTags}; + + use crate::named_repo; + + #[test] + fn default_uses_all_refs() -> crate::Result { + let repo = named_repo("make_commit_describe_multiple_tags.sh")?; + let actual = repo.head_commit()?.name_rev().format()?.to_string(); + assert!( + actual.starts_with("tags/"), + "default selection should consider tag refs before the branch name: {actual}" + ); + Ok(()) + } + + #[test] + fn tags_only_uses_tag_refs() -> crate::Result { + let repo = named_repo("make_commit_describe_multiple_tags.sh")?; + assert_eq!( + repo.head_commit()?.name_rev().names(AllTags).format()?.to_string(), + "tags/v2^0" + ); + Ok(()) + } + + #[test] + fn excluding_tags_allows_branch_names_to_win() -> crate::Result { + let repo = named_repo("make_commit_describe_multiple_tags.sh")?; + assert_eq!( + repo.head_commit()? + .name_rev() + .exclude_ref("tags/*") + .format()? + .to_string(), + "main" + ); + Ok(()) + } + + #[test] + fn include_patterns_limit_usable_refs() -> crate::Result { + let repo = named_repo("make_commit_describe_multiple_tags.sh")?; + assert_eq!( + repo.head_commit()? + .name_rev() + .names(AllRefs) + .include_ref("refs/tags/v4") + .format()? + .to_string(), + "tags/v4^0" + ); + assert_eq!( + repo.head_commit()? + .name_rev() + .names(AllRefs) + .include_ref("tags/v4") + .format()? + .to_string(), + "v4^0" + ); + Ok(()) + } + + #[test] + fn fallback_to_id_if_no_refs_match() -> crate::Result { + let repo = named_repo("make_commit_describe_multiple_tags.sh")?; + let mut name_rev = repo.head_commit()?.name_rev().exclude_ref("refs/*"); + assert!( + name_rev.try_format()?.is_none(), + "no symbolic name is available after excluding all refs" + ); + assert_eq!(name_rev.format()?.to_string().len(), 7); + Ok(()) + } + + #[test] + fn non_commit_refs_are_ignored() -> crate::Result { + let repo = named_repo("make_commit_name_rev_edge_cases.sh")?; + assert_eq!( + repo.head_commit()?.name_rev().names(AllTags).format()?.to_string(), + "tags/a^0" + ); + Ok(()) + } + + #[test] + fn older_tips_do_not_name_newer_targets() -> crate::Result { + let repo = named_repo("make_commit_name_rev_cutoff.sh")?; + let commit = repo + .find_reference("refs/heads/skew-target")? + .id() + .object()? + .into_commit(); + + assert!( + commit.name_rev().names(AllTags).try_format()?.is_none(), + "the skewed child tag is older than the target cutoff and should be ignored" + ); + assert_eq!(commit.name_rev().format()?.to_string(), "skew-target"); + Ok(()) + } } diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index d9ef65d66dd..d7df899eea0 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -1381,6 +1381,37 @@ pub fn main() -> Result<()> { ) }, ), + commit::Subcommands::NameRev { + tags, + refs, + exclude, + always, + no_undefined, + name_only, + rev_spec, + } => prepare_and_run( + "commit-name-rev", + trace, + verbose, + progress, + progress_keep_open, + None, + move |_progress, out, _err| { + core::repository::commit::name_rev( + repository(Mode::Strict)?, + rev_spec.as_deref(), + out, + core::repository::commit::name_rev::Options { + tags, + refs, + exclude, + always, + no_undefined, + name_only, + }, + ) + }, + ), }, Subcommands::Tag(platform) => match platform.cmds { Some(tag::Subcommands::List) | None => prepare_and_run( diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index f21eff79194..cc94f2ca584 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -990,6 +990,35 @@ pub mod commit { #[clap(short = 'd', long)] dirty_suffix: Option>, + /// A specification of the revision to use, or the current `HEAD` if unset. + rev_spec: Option, + }, + /// Find a symbolic name for the current commit or the given one. + NameRev { + /// Only use tags to name commits. + #[clap(long)] + tags: bool, + + /// Only use refs matching the given pattern. Can be passed multiple times. + #[clap(long = "refs")] + refs: Vec, + + /// Ignore refs matching the given pattern. Can be passed multiple times. + #[clap(long)] + exclude: Vec, + + /// Show an abbreviated commit object as fallback. + #[clap(long)] + always: bool, + + /// Fail if no name can be found. + #[clap(long = "no-undefined")] + no_undefined: bool, + + /// Print only the ref-based name. + #[clap(long)] + name_only: bool, + /// A specification of the revision to use, or the current `HEAD` if unset. rev_spec: Option, },