310 lines · 12.8 KB · Rust
RawDownload
//!#![allow(clippy::empty_docs)] use std::convert::Infallible; /// Commit signing errors.#[cfg(feature = "command")]pub mod sign;/// Commit signature verification errors.#[cfg(feature = "command")]pub mod verify; #[cfg(feature = "command")]fn signature_program(    config: &crate::config::Snapshot<'_>,    format: gix_object::signature::Format,) -> Result<std::ffi::OsString, gix_config::path::interpolate::Error> {    use crate::config::tree::{Gpg, Key, gpg};    use gix_object::signature::Format;     let (program, default) = match format {        Format::OpenPgp => (            match config.trusted_path(gpg::OpenPgp::PROGRAM)? {                Some(program) => Some(program),                None => config.trusted_path(Gpg::PROGRAM)?,            },            &gpg::OpenPgp::PROGRAM,        ),        Format::X509 => (config.trusted_path(gpg::X509::PROGRAM)?, &gpg::X509::PROGRAM),        Format::Ssh => (config.trusted_path(gpg::Ssh::PROGRAM)?, &gpg::Ssh::PROGRAM),    };    Ok(program        .unwrap_or_else(|| {            let default = gix_path::from_bstr(default.default_value_or_panic()).into_owned();            #[cfg(windows)]            if let Some(program) = default.to_str().and_then(gix_path::env::installation_program) {                return program;            }            default        })        .into_os_string())} /// An empty array of a type usable with the `gix::easy` API to help declaring no parents should be usedpub const NO_PARENT_IDS: [gix_hash::ObjectId; 0] = []; /// The error returned by [`commit(…)`](crate::Repository::commit()).#[derive(Debug, thiserror::Error)]#[expect(missing_docs)]pub enum Error {    #[error(transparent)]    ParseTime(#[from] crate::config::time::Error),    #[error("Committer identity is not configured")]    CommitterMissing,    #[error("Author identity is not configured")]    AuthorMissing,    #[error(transparent)]    ReferenceNameValidation(#[from] gix_ref::name::Error),    #[error(transparent)]    WriteObject(#[from] crate::object::write::Error),    #[error(transparent)]    ReferenceEdit(#[from] crate::reference::edit::Error),} impl From<std::convert::Infallible> for Error {    fn from(_value: Infallible) -> Self {        unreachable!("cannot be invoked")    }} ///#[cfg(feature = "revision")]pub mod describe {    use gix_error::Exn;    use gix_hash::ObjectId;    use gix_hashtable::HashMap;    use std::borrow::Cow;     use crate::{Repository, bstr::BStr, ext::ObjectIdExt};     /// The result of [`try_resolve()`][Platform::try_resolve()].    pub struct Resolution<'repo> {        /// The outcome of the describe operation.        pub outcome: gix_revision::describe::Outcome<'static>,        /// The id to describe.        pub id: crate::Id<'repo>,    }     impl Resolution<'_> {        /// Turn this instance into something displayable.        pub fn format(self) -> Result<gix_revision::describe::Format<'static>, Error> {            let prefix = self.id.shorten()?;            Ok(self.outcome.into_format(prefix.hex_len()))        }         /// Turn this instance into something displayable, possibly with dirty-suffix.        ///        /// If `dirty_suffix` is `Some(suffix)`, a possibly expensive [dirty check](crate::Repository::is_dirty()) will be        /// performed so that the `suffix` is appended to the output. If it is `None`, no check will be performed and        /// there will be no suffix.        /// Note that obtaining the dirty-state of the repository can be expensive.        #[cfg(feature = "status")]        pub fn format_with_dirty_suffix(            self,            dirty_suffix: impl Into<Option<String>>,        ) -> Result<gix_revision::describe::Format<'static>, Error> {            let prefix = self.id.shorten()?;            let mut dirty_suffix = dirty_suffix.into();            if dirty_suffix.is_some() && !self.id.repo.is_dirty()? {                dirty_suffix.take();            }            let mut format = self.outcome.into_format(prefix.hex_len());            format.dirty_suffix = dirty_suffix;            Ok(format)        }    }     /// The error returned by [`try_format()`][Platform::try_format()].    #[derive(Debug, thiserror::Error)]    #[expect(missing_docs)]    pub enum Error {        #[error(transparent)]        OpenCache(#[from] crate::repository::commit_graph_if_enabled::Error),        #[error(transparent)]        Describe(#[from] gix_revision::describe::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),        #[error(transparent)]        #[cfg(feature = "status")]        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<HashMap<ObjectId, Cow<'static, BStr>>, 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();                    // 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()                }                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::<BStr>::from(r.name().shorten().to_owned())))                        })                        .collect();                    // 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))                        .collect()                }            })        }    }     /// A support type to allow configuring a `git describe` 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) first_parent: bool,        pub(crate) id_as_fallback: bool,        pub(crate) max_candidates: usize,    }     impl<'repo> Platform<'repo> {        /// Configure which names to `select` from which describe can chose.        pub fn names(mut self, select: SelectRef) -> Self {            self.select = select;            self        }         /// If true, shorten the graph traversal time by just traversing the first parent of merge commits.        pub fn traverse_first_parent(mut self, first_parent: bool) -> Self {            self.first_parent = first_parent;            self        }         /// Only consider the given number of candidates, instead of the default of 10.        pub fn max_candidates(mut self, candidates: usize) -> Self {            self.max_candidates = candidates;            self        }         /// If true, even if no candidate 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, returning `Some(describe::Format)`        /// if one was found, or `None` if that wasn't the case.        pub fn try_format(&self) -> Result<Option<gix_revision::describe::Format<'static>>, 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.        ///        /// The outcome provides additional information, but leaves the caller with the burden        ///        /// # Performance        ///        /// It is greatly recommended to [assure an object cache is set](crate::Repository::object_cache_size_if_unset())        /// to save ~40% of time.        pub fn try_resolve_with_cache(            &self,            cache: Option<&'_ gix_commitgraph::Graph>,        ) -> Result<Option<Resolution<'repo>>, Error> {            let mut graph = self.repo.revision_graph(cache);            let outcome = gix_revision::describe(                &self.id,                &mut graph,                gix_revision::describe::Options {                    name_by_oid: self.select.names(self.repo)?,                    fallback_to_oid: self.id_as_fallback,                    first_parent: self.first_parent,                    max_candidates: self.max_candidates,                },            )            .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.        ///        /// # Performance        ///        /// Prefer to use the [`Self::try_resolve_with_cache()`] method when processing more than one commit at a time.        pub fn try_resolve(&self) -> Result<Option<Resolution<'repo>>, 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<gix_revision::describe::Format<'static>, Error> {            self.id_as_fallback = true;            Ok(self.try_format()?.expect("BUG: fallback must always produce a format"))        }    }}