Skip to main content

gix_traverse/commit/
simple.rs

1use std::{
2    cmp::{Ordering, Reverse},
3    collections::VecDeque,
4};
5
6use gix_date::SecondsSinceUnixEpoch;
7use gix_hash::ObjectId;
8use smallvec::SmallVec;
9
10#[derive(Default, Debug, Copy, Clone)]
11/// The order with which to prioritize the search.
12pub enum CommitTimeOrder {
13    #[default]
14    /// Sort commits by newest first.
15    NewestFirst,
16    /// Sort commits by oldest first.
17    #[doc(alias = "Sort::REVERSE", alias = "git2")]
18    OldestFirst,
19}
20
21/// Specify how to sort commits during a [simple](super::Simple) traversal.
22///
23/// ### Sample History
24///
25/// The following history will be referred to for explaining how the sort order works, with the number denoting the commit timestamp
26/// (*their X-alignment doesn't matter*).
27///
28/// ```text
29/// ---1----2----4----7 <- second parent of 8
30///     \              \
31///      3----5----6----8---
32/// ```
33#[derive(Default, Debug, Copy, Clone)]
34pub enum Sorting {
35    /// Commits are sorted as they are mentioned in the commit graph.
36    ///
37    /// In the *sample history* the order would be `8, 6, 7, 5, 4, 3, 2, 1`.
38    ///
39    /// ### Note
40    ///
41    /// This is not to be confused with `git log/rev-list --topo-order`, which is notably different from
42    /// as it avoids overlapping branches.
43    #[default]
44    BreadthFirst,
45    /// Commits are sorted by their commit time in the order specified, either newest or oldest first.
46    ///
47    /// The sorting applies to all currently queued commit ids and thus is full.
48    ///
49    /// In the *sample history* the order would be `8, 7, 6, 5, 4, 3, 2, 1` for [`NewestFirst`](CommitTimeOrder::NewestFirst),
50    /// or `1, 2, 3, 4, 5, 6, 7, 8` for [`OldestFirst`](CommitTimeOrder::OldestFirst).
51    ///
52    /// # Performance
53    ///
54    /// This mode benefits greatly from having an object_cache in `find()`
55    /// to avoid having to lookup each commit twice.
56    ByCommitTime(CommitTimeOrder),
57    /// This sorting is similar to [`ByCommitTime`](Sorting::ByCommitTime), but adds a cutoff to not return commits older than
58    /// a given time, stopping the iteration once no younger commits is queued to be traversed.
59    ///
60    /// As the query is usually repeated with different cutoff dates, this search mode benefits greatly from an object cache.
61    ///
62    /// In the *sample history* and a cut-off date of 4, the returned list of commits would be `8, 7, 6, 4`.
63    ByCommitTimeCutoff {
64        /// The order in which to prioritize lookups.
65        order: CommitTimeOrder,
66        /// The number of seconds since unix epoch, the same value obtained by any `gix_date::Time` structure and the way git counts time.
67        seconds: gix_date::SecondsSinceUnixEpoch,
68    },
69}
70
71/// The error is part of the item returned by the [Ancestors](super::Simple) iterator.
72#[derive(Debug, thiserror::Error)]
73#[allow(missing_docs)]
74pub enum Error {
75    #[error(transparent)]
76    Find(#[from] gix_object::find::existing_iter::Error),
77    #[error(transparent)]
78    ObjectDecode(#[from] gix_object::decode::Error),
79    #[error(transparent)]
80    HiddenGraph(#[from] gix_revwalk::graph::get_or_insert_default::Error),
81}
82
83use Result as Either;
84
85type QueueKey<T> = Either<T, Reverse<T>>;
86type CommitDateQueue = gix_revwalk::PriorityQueue<QueueKey<SecondsSinceUnixEpoch>, ObjectId>;
87
88bitflags::bitflags! {
89    #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
90    struct PaintFlags: u8 {
91        const VISIBLE = 1 << 0;
92        const HIDDEN = 1 << 1;
93        const STALE = 1 << 2;
94    }
95}
96
97/// Priority for hidden-frontier painting that prefers newer commits, using generation numbers
98/// when available and falling back to commit time as a tie-breaker.
99#[derive(Debug, Clone, Copy, Eq, PartialEq)]
100struct GenThenTime {
101    generation: gix_revwalk::graph::Generation,
102    time: SecondsSinceUnixEpoch,
103}
104
105impl From<&gix_revwalk::graph::Commit<PaintFlags>> for GenThenTime {
106    fn from(commit: &gix_revwalk::graph::Commit<PaintFlags>) -> Self {
107        GenThenTime {
108            generation: commit.generation.unwrap_or(gix_commitgraph::GENERATION_NUMBER_INFINITY),
109            time: commit.commit_time,
110        }
111    }
112}
113
114impl Ord for GenThenTime {
115    fn cmp(&self, other: &Self) -> Ordering {
116        self.generation.cmp(&other.generation).then(self.time.cmp(&other.time))
117    }
118}
119
120impl PartialOrd<Self> for GenThenTime {
121    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
122        Some(self.cmp(other))
123    }
124}
125
126/// The state used and potentially shared by multiple graph traversals.
127#[derive(Clone)]
128pub(super) struct State {
129    /// Pending visible commits when traversal is driven in insertion/topological order.
130    ///
131    /// This queue is consumed by `next_by_topology()`, and also becomes the active frontier for
132    /// first-parent traversal after any time-ordered queue is flattened back into FIFO order.
133    next: VecDeque<ObjectId>,
134    /// Pending visible commits when traversal is driven by commit date.
135    ///
136    /// This queue is consumed by `next_by_commit_date()`. It holds the same logical frontier as
137    /// `next`, but keeps it ordered by commit time instead of insertion order.
138    queue: CommitDateQueue,
139    /// Backing storage for the currently yielded commit.
140    buf: Vec<u8>,
141    /// Set of commits that were already enqueued for the visible traversal, for cycle-checking.
142    seen: gix_hashtable::HashSet<ObjectId>,
143    /// Hidden frontier commits that must not be yielded or crossed during traversal.
144    hidden: gix_revwalk::graph::IdMap<()>,
145    /// Hidden input tips from which the hidden frontier is derived.
146    ///
147    /// These are consumed on the first call ot `next` to compute the hidden frontier once.
148    hidden_tips: Vec<ObjectId>,
149    /// Scratch buffer for parent commit lookups when commit times are loaded from the object database.
150    parents_buf: Vec<u8>,
151    /// Reusable parent id/time storage populated from the commit-graph cache.
152    parent_ids: SmallVec<[(ObjectId, SecondsSinceUnixEpoch); 2]>,
153}
154
155fn to_queue_key(i: i64, order: CommitTimeOrder) -> QueueKey<i64> {
156    match order {
157        CommitTimeOrder::NewestFirst => Ok(i),
158        CommitTimeOrder::OldestFirst => Err(Reverse(i)),
159    }
160}
161
162/// Compute the boundary at which the visible walk must stop because commits become reachable from
163/// both the visible tips and the hidden tips.
164///
165/// The algorithm performs a merge-base-style paint in a temporary `gix_revwalk::Graph`:
166/// visible tips are marked with `VISIBLE`, hidden tips with `HIDDEN`, and these flags are
167/// propagated to parents in generation/time order. Once a commit carries both flags, it is part
168/// of the overlap between the two histories and is marked `STALE` so older ancestors no longer
169/// need to keep re-propagating the same combined state.
170///
171/// The returned set is not all commits reachable from hidden tips. It is only the overlap frontier
172/// where the visible traversal must stop. The actual `Simple` walk then skips these commits and
173/// refuses to enqueue parents across them, which avoids traversing hidden-only history.
174fn compute_hidden_frontier(
175    visible_tips: &[ObjectId],
176    hidden_tips: &[ObjectId],
177    objects: &impl gix_object::Find,
178    cache: Option<&gix_commitgraph::Graph>,
179) -> Result<gix_revwalk::graph::IdMap<()>, Error> {
180    let mut graph = gix_revwalk::Graph::<gix_revwalk::graph::Commit<PaintFlags>>::new(objects, cache);
181    let mut queue = gix_revwalk::PriorityQueue::<GenThenTime, ObjectId>::new();
182
183    for &visible in visible_tips {
184        graph.get_or_insert_full_commit(visible, |commit| {
185            commit.data |= PaintFlags::VISIBLE;
186            queue.insert(GenThenTime::from(&*commit), visible);
187        })?;
188    }
189    for &hidden in hidden_tips {
190        graph.get_or_insert_full_commit(hidden, |commit| {
191            commit.data |= PaintFlags::HIDDEN;
192            queue.insert(GenThenTime::from(&*commit), hidden);
193        })?;
194    }
195
196    while queue.iter_unordered().any(|id| {
197        graph
198            .get(id)
199            .is_some_and(|commit| !commit.data.contains(PaintFlags::STALE))
200    }) {
201        let (_info, commit_id) = match queue.pop() {
202            Some(v) => v,
203            None => break,
204        };
205        let commit = graph.get_mut(&commit_id).expect("queued commits are in the graph");
206        let mut flags = commit.data;
207        if flags == (PaintFlags::VISIBLE | PaintFlags::HIDDEN) {
208            flags |= PaintFlags::STALE;
209        }
210
211        for parent_id in commit.parents.clone() {
212            graph.get_or_insert_full_commit(parent_id, |parent| {
213                if (parent.data & flags) != flags {
214                    parent.data |= flags;
215                    queue.insert(GenThenTime::from(&*parent), parent_id);
216                }
217            })?;
218        }
219    }
220
221    Ok(graph
222        .detach()
223        .into_iter()
224        .filter_map(|(id, commit)| {
225            commit
226                .data
227                .contains(PaintFlags::VISIBLE | PaintFlags::HIDDEN)
228                .then_some((id, ()))
229        })
230        .collect())
231}
232
233///
234mod init {
235    use super::{
236        collect_parents, compute_hidden_frontier, to_queue_key, CommitDateQueue, CommitTimeOrder, Error, Sorting, State,
237    };
238    use crate::commit::{Either, Info, ParentIds, Parents, Simple};
239    use gix_date::SecondsSinceUnixEpoch;
240    use gix_hash::{oid, ObjectId};
241    use gix_object::{CommitRefIter, FindExt};
242    use std::{cmp::Reverse, collections::VecDeque};
243
244    impl Default for State {
245        fn default() -> Self {
246            State {
247                next: Default::default(),
248                queue: gix_revwalk::PriorityQueue::new(),
249                buf: vec![],
250                seen: Default::default(),
251                hidden: Default::default(),
252                hidden_tips: Vec::new(),
253                parents_buf: vec![],
254                parent_ids: Default::default(),
255            }
256        }
257    }
258
259    impl State {
260        fn clear(&mut self) {
261            let Self {
262                next,
263                queue,
264                buf,
265                seen,
266                hidden,
267                hidden_tips,
268                parents_buf: _,
269                parent_ids: _,
270            } = self;
271            next.clear();
272            queue.clear();
273            buf.clear();
274            seen.clear();
275            hidden.clear();
276            hidden_tips.clear();
277        }
278    }
279
280    impl Sorting {
281        /// If not topo sort, provide the cutoff date if present.
282        fn cutoff_time(&self) -> Option<SecondsSinceUnixEpoch> {
283            match self {
284                Sorting::ByCommitTimeCutoff { seconds, .. } => Some(*seconds),
285                _ => None,
286            }
287        }
288    }
289
290    /// Builder methods
291    impl<Find, Predicate> Simple<Find, Predicate>
292    where
293        Find: gix_object::Find,
294    {
295        /// Set the `sorting` method.
296        pub fn sorting(mut self, sorting: Sorting) -> Result<Self, Error> {
297            self.sorting = sorting;
298            match self.sorting {
299                Sorting::BreadthFirst => self.queue_to_vecdeque(),
300                Sorting::ByCommitTime(order) | Sorting::ByCommitTimeCutoff { order, .. } => {
301                    let state = &mut self.state;
302                    for commit_id in state.next.drain(..) {
303                        add_to_queue(
304                            commit_id,
305                            order,
306                            sorting.cutoff_time(),
307                            &mut state.queue,
308                            &self.objects,
309                            &mut state.buf,
310                        )?;
311                    }
312                }
313            }
314            Ok(self)
315        }
316
317        /// Change our commit parent handling mode to the given one.
318        pub fn parents(mut self, mode: Parents) -> Self {
319            self.parents = mode;
320            if matches!(self.parents, Parents::First) {
321                self.queue_to_vecdeque();
322            }
323            self
324        }
325
326        /// Hide the given `tips`, along with all commits reachable by them so that they will not be returned
327        /// by the traversal.
328        pub fn hide(mut self, tips: impl IntoIterator<Item = ObjectId>) -> Result<Self, Error> {
329            self.state.hidden_tips = tips.into_iter().collect();
330            Ok(self)
331        }
332
333        /// Set the commitgraph as `cache` to greatly accelerate any traversal.
334        ///
335        /// The cache will be used if possible, but we will fall back without error to using the object
336        /// database for commit lookup. If the cache is corrupt, we will fall back to the object database as well.
337        pub fn commit_graph(mut self, cache: Option<gix_commitgraph::Graph>) -> Self {
338            self.cache = cache;
339            self
340        }
341
342        fn queue_to_vecdeque(&mut self) {
343            let state = &mut self.state;
344            state.next.extend(
345                std::mem::replace(&mut state.queue, gix_revwalk::PriorityQueue::new())
346                    .into_iter_unordered()
347                    .map(|(_time, id)| id),
348            );
349        }
350
351        fn visible_inputs_sorted(&self) -> Vec<ObjectId> {
352            let mut out: Vec<_> = self
353                .state
354                .next
355                .iter()
356                .copied()
357                .chain(self.state.queue.iter_unordered().copied())
358                .collect();
359            out.sort();
360            out.dedup();
361            out
362        }
363
364        fn compute_hidden_frontier(&mut self, hidden_tips: Vec<ObjectId>) -> Result<(), Error> {
365            self.state.hidden.clear();
366            if hidden_tips.is_empty() {
367                return Ok(());
368            }
369            let visible_tips = self.visible_inputs_sorted();
370            if visible_tips.is_empty() {
371                return Ok(());
372            }
373            self.state.hidden =
374                compute_hidden_frontier(&visible_tips, &hidden_tips, &self.objects, self.cache.as_ref())?;
375            self.state.next.retain(|id| !self.state.hidden.contains_key(id));
376            self.state.queue = std::mem::replace(&mut self.state.queue, gix_revwalk::PriorityQueue::new())
377                .into_iter_unordered()
378                .filter(|(_, id)| !self.state.hidden.contains_key(id))
379                .collect();
380            Ok(())
381        }
382    }
383
384    fn add_to_queue(
385        commit_id: ObjectId,
386        order: CommitTimeOrder,
387        cutoff_time: Option<SecondsSinceUnixEpoch>,
388        queue: &mut CommitDateQueue,
389        objects: &impl gix_object::Find,
390        buf: &mut Vec<u8>,
391    ) -> Result<(), Error> {
392        let commit_iter = objects.find_commit_iter(&commit_id, buf)?;
393        let time = commit_iter.committer()?.seconds();
394        let key = to_queue_key(time, order);
395        match (cutoff_time, order) {
396            (Some(cutoff_time), _) if time >= cutoff_time => queue.insert(key, commit_id),
397            (Some(_), _) => {}
398            (None, _) => queue.insert(key, commit_id),
399        }
400        Ok(())
401    }
402
403    /// Lifecycle methods
404    impl<Find> Simple<Find, fn(&oid) -> bool>
405    where
406        Find: gix_object::Find,
407    {
408        /// Create a new instance.
409        ///
410        /// * `find` - a way to lookup new object data during traversal by their `ObjectId`, writing their data into buffer and returning
411        ///   an iterator over commit tokens if the object is present and is a commit. Caching should be implemented within this function
412        ///   as needed.
413        /// * `tips`
414        ///   * the starting points of the iteration, usually commits
415        ///   * each commit they lead to will only be returned once, including the tip that started it
416        pub fn new(tips: impl IntoIterator<Item = impl Into<ObjectId>>, find: Find) -> Self {
417            Self::filtered(tips, find, |_| true)
418        }
419    }
420
421    impl<Find, Predicate> Simple<Find, Predicate>
422    where
423        Find: gix_object::Find,
424        Predicate: FnMut(&oid) -> bool,
425    {
426        /// Create a new instance with commit filtering enabled.
427        ///
428        /// * `find` - a way to lookup new object data during traversal by their `ObjectId`, writing their data into buffer and returning
429        ///   an iterator over commit tokens if the object is present and is a commit. Caching should be implemented within this function
430        ///   as needed.
431        /// * `tips`
432        ///   * the starting points of the iteration, usually commits
433        ///   * each commit they lead to will only be returned once, including the tip that started it
434        /// * `predicate` - indicate whether a given commit should be included in the result as well
435        ///   as whether its parent commits should be traversed.
436        pub fn filtered(
437            tips: impl IntoIterator<Item = impl Into<ObjectId>>,
438            find: Find,
439            mut predicate: Predicate,
440        ) -> Self {
441            let tips = tips.into_iter();
442            let mut state = State::default();
443            {
444                state.clear();
445                state.next.reserve(tips.size_hint().0);
446                for tip in tips.map(Into::into) {
447                    if state.seen.insert(tip) && predicate(&tip) {
448                        state.next.push_back(tip);
449                    }
450                }
451            }
452            Self {
453                objects: find,
454                cache: None,
455                predicate,
456                state,
457                parents: Default::default(),
458                sorting: Default::default(),
459            }
460        }
461    }
462
463    /// Access
464    impl<Find, Predicate> Simple<Find, Predicate> {
465        /// Return an iterator for accessing data of the current commit, parsed lazily.
466        pub fn commit_iter(&self) -> CommitRefIter<'_> {
467            CommitRefIter::from_bytes(self.commit_data())
468        }
469
470        /// Return the current commits' raw data, which can be parsed using [`gix_object::CommitRef::from_bytes()`].
471        pub fn commit_data(&self) -> &[u8] {
472            &self.state.buf
473        }
474    }
475
476    impl<Find, Predicate> Iterator for Simple<Find, Predicate>
477    where
478        Find: gix_object::Find,
479        Predicate: FnMut(&oid) -> bool,
480    {
481        type Item = Result<Info, Error>;
482
483        fn next(&mut self) -> Option<Self::Item> {
484            if !self.state.hidden_tips.is_empty() {
485                let hidden_tips = std::mem::take(&mut self.state.hidden_tips);
486                if let Err(err) = self.compute_hidden_frontier(hidden_tips) {
487                    self.state.queue.clear();
488                    self.state.next.clear();
489                    return Some(Err(err));
490                }
491            }
492            if matches!(self.parents, Parents::First) {
493                self.next_by_topology()
494            } else {
495                match self.sorting {
496                    Sorting::BreadthFirst => self.next_by_topology(),
497                    Sorting::ByCommitTime(order) => self.next_by_commit_date(order, None),
498                    Sorting::ByCommitTimeCutoff { seconds, order } => self.next_by_commit_date(order, seconds.into()),
499                }
500            }
501        }
502    }
503
504    /// Utilities
505    impl<Find, Predicate> Simple<Find, Predicate>
506    where
507        Find: gix_object::Find,
508        Predicate: FnMut(&oid) -> bool,
509    {
510        fn next_by_commit_date(
511            &mut self,
512            order: CommitTimeOrder,
513            cutoff: Option<SecondsSinceUnixEpoch>,
514        ) -> Option<Result<Info, Error>> {
515            let state = &mut self.state;
516            let next = &mut state.queue;
517
518            loop {
519                let (commit_time, oid) = match next.pop()? {
520                    (Ok(t) | Err(Reverse(t)), o) => (t, o),
521                };
522                if state.hidden.contains_key(&oid) {
523                    continue;
524                }
525                let mut parents: ParentIds = Default::default();
526
527                match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) {
528                    Ok(Either::CachedCommit(commit)) => {
529                        if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) {
530                            // drop corrupt caches and try again with ODB
531                            self.cache = None;
532                            return self.next_by_commit_date(order, cutoff);
533                        }
534                        for (id, parent_commit_time) in state.parent_ids.drain(..) {
535                            parents.push(id);
536                            insert_into_seen_and_queue(
537                                &mut state.seen,
538                                &state.hidden,
539                                id,
540                                &mut self.predicate,
541                                next,
542                                order,
543                                cutoff,
544                                || parent_commit_time,
545                            );
546                        }
547                    }
548                    Ok(Either::CommitRefIter(commit_iter)) => {
549                        for token in commit_iter {
550                            match token {
551                                Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue,
552                                Ok(gix_object::commit::ref_iter::Token::Parent { id }) => {
553                                    parents.push(id);
554                                    insert_into_seen_and_queue(
555                                        &mut state.seen,
556                                        &state.hidden,
557                                        id,
558                                        &mut self.predicate,
559                                        next,
560                                        order,
561                                        cutoff,
562                                        || {
563                                            let parent =
564                                                self.objects.find_commit_iter(id.as_ref(), &mut state.parents_buf).ok();
565                                            parent
566                                                .and_then(|parent| {
567                                                    parent.committer().ok().map(|committer| committer.seconds())
568                                                })
569                                                .unwrap_or_default()
570                                        },
571                                    );
572                                }
573                                Ok(_unused_token) => break,
574                                Err(err) => return Some(Err(err.into())),
575                            }
576                        }
577                    }
578                    Err(err) => return Some(Err(err.into())),
579                }
580
581                return Some(Ok(Info {
582                    id: oid,
583                    parent_ids: parents,
584                    commit_time: Some(commit_time),
585                }));
586            }
587        }
588
589        fn next_by_topology(&mut self) -> Option<Result<Info, Error>> {
590            let state = &mut self.state;
591            let next = &mut state.next;
592
593            loop {
594                let oid = next.pop_front()?;
595                if state.hidden.contains_key(&oid) {
596                    continue;
597                }
598                let mut parents: ParentIds = Default::default();
599
600                match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) {
601                    Ok(Either::CachedCommit(commit)) => {
602                        if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) {
603                            // drop corrupt caches and try again with ODB
604                            self.cache = None;
605                            return self.next_by_topology();
606                        }
607
608                        for (pid, _commit_time) in state.parent_ids.drain(..) {
609                            parents.push(pid);
610                            insert_into_seen_and_next(&mut state.seen, &state.hidden, pid, &mut self.predicate, next);
611                            if matches!(self.parents, Parents::First) {
612                                break;
613                            }
614                        }
615                    }
616                    Ok(Either::CommitRefIter(commit_iter)) => {
617                        for token in commit_iter {
618                            match token {
619                                Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue,
620                                Ok(gix_object::commit::ref_iter::Token::Parent { id: pid }) => {
621                                    parents.push(pid);
622                                    insert_into_seen_and_next(
623                                        &mut state.seen,
624                                        &state.hidden,
625                                        pid,
626                                        &mut self.predicate,
627                                        next,
628                                    );
629                                    if matches!(self.parents, Parents::First) {
630                                        break;
631                                    }
632                                }
633                                Ok(_a_token_past_the_parents) => break,
634                                Err(err) => return Some(Err(err.into())),
635                            }
636                        }
637                    }
638                    Err(err) => return Some(Err(err.into())),
639                }
640
641                return Some(Ok(Info {
642                    id: oid,
643                    parent_ids: parents,
644                    commit_time: None,
645                }));
646            }
647        }
648    }
649
650    fn insert_into_seen_and_next(
651        seen: &mut gix_hashtable::HashSet<ObjectId>,
652        hidden: &gix_revwalk::graph::IdMap<()>,
653        parent_id: ObjectId,
654        predicate: &mut impl FnMut(&oid) -> bool,
655        next: &mut VecDeque<ObjectId>,
656    ) {
657        if hidden.contains_key(&parent_id) {
658            return;
659        }
660        if seen.insert(parent_id) && predicate(&parent_id) {
661            next.push_back(parent_id);
662        }
663    }
664
665    #[allow(clippy::too_many_arguments)]
666    fn insert_into_seen_and_queue(
667        seen: &mut gix_hashtable::HashSet<ObjectId>,
668        hidden: &gix_revwalk::graph::IdMap<()>,
669        parent_id: ObjectId,
670        predicate: &mut impl FnMut(&oid) -> bool,
671        queue: &mut CommitDateQueue,
672        order: CommitTimeOrder,
673        cutoff: Option<SecondsSinceUnixEpoch>,
674        get_parent_commit_time: impl FnOnce() -> gix_date::SecondsSinceUnixEpoch,
675    ) {
676        if hidden.contains_key(&parent_id) {
677            return;
678        }
679        if seen.insert(parent_id) && predicate(&parent_id) {
680            let parent_commit_time = get_parent_commit_time();
681            let key = to_queue_key(parent_commit_time, order);
682            match cutoff {
683                Some(cutoff_older_than) if parent_commit_time < cutoff_older_than => {}
684                Some(_) | None => queue.insert(key, parent_id),
685            }
686        }
687    }
688}
689
690fn collect_parents(
691    dest: &mut SmallVec<[(gix_hash::ObjectId, gix_date::SecondsSinceUnixEpoch); 2]>,
692    cache: Option<&gix_commitgraph::Graph>,
693    parents: gix_commitgraph::file::commit::Parents<'_>,
694) -> bool {
695    dest.clear();
696    let cache = cache.as_ref().expect("parents iter is available, backed by `cache`");
697    for parent_id in parents {
698        match parent_id {
699            Ok(pos) => dest.push({
700                let parent = cache.commit_at(pos);
701                (
702                    parent.id().to_owned(),
703                    parent.committer_timestamp() as gix_date::SecondsSinceUnixEpoch,
704                )
705            }),
706            Err(_err) => return false,
707        }
708    }
709    true
710}