Skip to main content

gix/
commit.rs

1//!
2#![allow(clippy::empty_docs)]
3
4use std::convert::Infallible;
5
6/// An empty array of a type usable with the `gix::easy` API to help declaring no parents should be used
7pub const NO_PARENT_IDS: [gix_hash::ObjectId; 0] = [];
8
9/// The error returned by [`commit(…)`](crate::Repository::commit()).
10#[derive(Debug, thiserror::Error)]
11#[allow(missing_docs)]
12pub enum Error {
13    #[error(transparent)]
14    ParseTime(#[from] crate::config::time::Error),
15    #[error("Committer identity is not configured")]
16    CommitterMissing,
17    #[error("Author identity is not configured")]
18    AuthorMissing,
19    #[error(transparent)]
20    ReferenceNameValidation(#[from] gix_ref::name::Error),
21    #[error(transparent)]
22    WriteObject(#[from] crate::object::write::Error),
23    #[error(transparent)]
24    ReferenceEdit(#[from] crate::reference::edit::Error),
25}
26
27impl From<std::convert::Infallible> for Error {
28    fn from(_value: Infallible) -> Self {
29        unreachable!("cannot be invoked")
30    }
31}
32
33///
34#[cfg(feature = "revision")]
35pub mod describe {
36    use gix_error::Exn;
37    use gix_hash::ObjectId;
38    use gix_hashtable::HashMap;
39    use std::borrow::Cow;
40
41    use crate::{bstr::BStr, ext::ObjectIdExt, Repository};
42
43    /// The result of [`try_resolve()`][Platform::try_resolve()].
44    pub struct Resolution<'repo> {
45        /// The outcome of the describe operation.
46        pub outcome: gix_revision::describe::Outcome<'static>,
47        /// The id to describe.
48        pub id: crate::Id<'repo>,
49    }
50
51    impl Resolution<'_> {
52        /// Turn this instance into something displayable.
53        pub fn format(self) -> Result<gix_revision::describe::Format<'static>, Error> {
54            let prefix = self.id.shorten()?;
55            Ok(self.outcome.into_format(prefix.hex_len()))
56        }
57
58        /// Turn this instance into something displayable, possibly with dirty-suffix.
59        ///
60        /// If `dirty_suffix` is `Some(suffix)`, a possibly expensive [dirty check](crate::Repository::is_dirty()) will be
61        /// performed so that the `suffix` is appended to the output. If it is `None`, no check will be performed and
62        /// there will be no suffix.
63        /// Note that obtaining the dirty-state of the repository can be expensive.
64        #[cfg(feature = "status")]
65        pub fn format_with_dirty_suffix(
66            self,
67            dirty_suffix: impl Into<Option<String>>,
68        ) -> Result<gix_revision::describe::Format<'static>, Error> {
69            let prefix = self.id.shorten()?;
70            let mut dirty_suffix = dirty_suffix.into();
71            if dirty_suffix.is_some() && !self.id.repo.is_dirty()? {
72                dirty_suffix.take();
73            }
74            let mut format = self.outcome.into_format(prefix.hex_len());
75            format.dirty_suffix = dirty_suffix;
76            Ok(format)
77        }
78    }
79
80    /// The error returned by [`try_format()`][Platform::try_format()].
81    #[derive(Debug, thiserror::Error)]
82    #[allow(missing_docs)]
83    pub enum Error {
84        #[error(transparent)]
85        OpenCache(#[from] crate::repository::commit_graph_if_enabled::Error),
86        #[error(transparent)]
87        Describe(#[from] gix_revision::describe::Error),
88        #[error("Could not produce an unambiguous shortened id for formatting.")]
89        ShortId(#[from] crate::id::shorten::Error),
90        #[error(transparent)]
91        RefIter(#[from] crate::reference::iter::Error),
92        #[error(transparent)]
93        RefIterInit(#[from] crate::reference::iter::init::Error),
94        #[error(transparent)]
95        #[cfg(feature = "status")]
96        DetermineIsDirty(#[from] crate::status::is_dirty::Error),
97    }
98
99    /// A selector to choose what kind of references should contribute to names.
100    #[derive(Default, Debug, Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)]
101    pub enum SelectRef {
102        /// Only use annotated tags for names.
103        #[default]
104        AnnotatedTags,
105        /// Use all tags for names, annotated or plain reference.
106        AllTags,
107        /// Use all references, including local branch names.
108        AllRefs,
109    }
110
111    impl SelectRef {
112        fn names(&self, repo: &Repository) -> Result<HashMap<ObjectId, Cow<'static, BStr>>, Error> {
113            let platform = repo.references()?;
114
115            Ok(match self {
116                SelectRef::AllTags | SelectRef::AllRefs => {
117                    let mut refs: Vec<_> = match self {
118                        SelectRef::AllRefs => platform.all()?,
119                        SelectRef::AllTags => platform.tags()?,
120                        _ => unreachable!(),
121                    }
122                    .filter_map(Result::ok)
123                    .filter_map(|mut r: crate::Reference<'_>| {
124                        let target_id = r.target().try_id().map(ToOwned::to_owned);
125                        let peeled_id = r.peel_to_id().ok()?;
126                        let (prio, tag_time) = match target_id {
127                            Some(target_id) if peeled_id != *target_id => {
128                                let tag = repo.find_object(target_id).ok()?.try_into_tag().ok()?;
129                                let tag_time = tag.tagger().ok().and_then(|s| s.map(|s| s.seconds())).unwrap_or(0);
130                                (1, tag_time)
131                            }
132                            _ => (0, 0),
133                        };
134                        (
135                            peeled_id.inner,
136                            prio,
137                            tag_time,
138                            Cow::from(r.inner.name.shorten().to_owned()),
139                        )
140                            .into()
141                    })
142                    .collect();
143                    // By priority, then by time ascending, then lexicographically.
144                    // More recent entries overwrite older ones due to collection into hashmap.
145                    refs.sort_by(
146                        |(_a_peeled_id, a_prio, a_time, a_name), (_b_peeled_id, b_prio, b_time, b_name)| {
147                            a_prio
148                                .cmp(b_prio)
149                                .then_with(|| a_time.cmp(b_time))
150                                .then_with(|| b_name.cmp(a_name))
151                        },
152                    );
153                    refs.into_iter().map(|(a, _, _, b)| (a, b)).collect()
154                }
155                SelectRef::AnnotatedTags => {
156                    let mut peeled_commits_and_tag_date: Vec<_> = platform
157                        .tags()?
158                        .filter_map(Result::ok)
159                        .filter_map(|r: crate::Reference<'_>| {
160                            // TODO: we assume direct refs for tags, which is the common case, but it doesn't have to be
161                            //       so rather follow symrefs till the first object and then peel tags after the first object was found.
162                            let tag = r.try_id()?.object().ok()?.try_into_tag().ok()?;
163                            let tag_time = tag.tagger().ok().and_then(|s| s.map(|s| s.seconds())).unwrap_or(0);
164                            let commit_id = tag.target_id().ok()?.object().ok()?.try_into_commit().ok()?.id;
165                            Some((commit_id, tag_time, Cow::<BStr>::from(r.name().shorten().to_owned())))
166                        })
167                        .collect();
168                    // Sort by time ascending, then lexicographically.
169                    // More recent entries overwrite older ones due to collection into hashmap.
170                    peeled_commits_and_tag_date.sort_by(|(_a_id, a_time, a_name), (_b_id, b_time, b_name)| {
171                        a_time.cmp(b_time).then_with(|| b_name.cmp(a_name))
172                    });
173                    peeled_commits_and_tag_date
174                        .into_iter()
175                        .map(|(a, _, c)| (a, c))
176                        .collect()
177                }
178            })
179        }
180    }
181
182    /// A support type to allow configuring a `git describe` operation
183    pub struct Platform<'repo> {
184        pub(crate) id: gix_hash::ObjectId,
185        /// The owning repository.
186        pub repo: &'repo crate::Repository,
187        pub(crate) select: SelectRef,
188        pub(crate) first_parent: bool,
189        pub(crate) id_as_fallback: bool,
190        pub(crate) max_candidates: usize,
191    }
192
193    impl<'repo> Platform<'repo> {
194        /// Configure which names to `select` from which describe can chose.
195        pub fn names(mut self, select: SelectRef) -> Self {
196            self.select = select;
197            self
198        }
199
200        /// If true, shorten the graph traversal time by just traversing the first parent of merge commits.
201        pub fn traverse_first_parent(mut self, first_parent: bool) -> Self {
202            self.first_parent = first_parent;
203            self
204        }
205
206        /// Only consider the given number of candidates, instead of the default of 10.
207        pub fn max_candidates(mut self, candidates: usize) -> Self {
208            self.max_candidates = candidates;
209            self
210        }
211
212        /// If true, even if no candidate is available a format will always be produced.
213        pub fn id_as_fallback(mut self, use_fallback: bool) -> Self {
214            self.id_as_fallback = use_fallback;
215            self
216        }
217
218        /// Try to find a name for the configured commit id using all prior configuration, returning `Some(describe::Format)`
219        /// if one was found, or `None` if that wasn't the case.
220        pub fn try_format(&self) -> Result<Option<gix_revision::describe::Format<'static>>, Error> {
221            self.try_resolve()?.map(Resolution::format).transpose()
222        }
223
224        /// Try to find a name for the configured commit id using all prior configuration, returning `Some(Outcome)`
225        /// if one was found.
226        ///
227        /// The outcome provides additional information, but leaves the caller with the burden
228        ///
229        /// # Performance
230        ///
231        /// It is greatly recommended to [assure an object cache is set](crate::Repository::object_cache_size_if_unset())
232        /// to save ~40% of time.
233        pub fn try_resolve_with_cache(
234            &self,
235            cache: Option<&'_ gix_commitgraph::Graph>,
236        ) -> Result<Option<Resolution<'repo>>, Error> {
237            let mut graph = self.repo.revision_graph(cache);
238            let outcome = gix_revision::describe(
239                &self.id,
240                &mut graph,
241                gix_revision::describe::Options {
242                    name_by_oid: self.select.names(self.repo)?,
243                    fallback_to_oid: self.id_as_fallback,
244                    first_parent: self.first_parent,
245                    max_candidates: self.max_candidates,
246                },
247            )
248            .map_err(Exn::into_inner)?;
249
250            Ok(outcome.map(|outcome| Resolution {
251                outcome,
252                id: self.id.attach(self.repo),
253            }))
254        }
255
256        /// Like [`Self::try_resolve_with_cache()`], but obtains the commitgraph-cache internally for a single use.
257        ///
258        /// # Performance
259        ///
260        /// Prefer to use the [`Self::try_resolve_with_cache()`] method when processing more than one commit at a time.
261        pub fn try_resolve(&self) -> Result<Option<Resolution<'repo>>, Error> {
262            let cache = self.repo.commit_graph_if_enabled()?;
263            self.try_resolve_with_cache(cache.as_ref())
264        }
265
266        /// Like [`try_format()`](Self::try_format()), but turns `id_as_fallback()` on to always produce a format.
267        pub fn format(&mut self) -> Result<gix_revision::describe::Format<'static>, Error> {
268            self.id_as_fallback = true;
269            Ok(self.try_format()?.expect("BUG: fallback must always produce a format"))
270        }
271    }
272}