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    /// The object hash kind of the currently yielded commit data in `buf`.
142    /// It's used to know the kind of hash to expect when a new iterator is returned from `buf`
143    /// via `Simple::commit_iter()`.
144    object_hash: gix_hash::Kind,
145    /// Set of commits that were already enqueued for the visible traversal, for cycle-checking.
146    seen: gix_hashtable::HashSet<ObjectId>,
147    /// Hidden frontier commits that must not be yielded or crossed during traversal.
148    hidden: gix_revwalk::graph::IdMap<()>,
149    /// Hidden input tips from which the hidden frontier is derived.
150    ///
151    /// These are consumed on the first call ot `next` to compute the hidden frontier once.
152    hidden_tips: Vec<ObjectId>,
153    /// Scratch buffer for parent commit lookups when commit times are loaded from the object database.
154    parents_buf: Vec<u8>,
155    /// Reusable parent id/time storage populated from the commit-graph cache.
156    parent_ids: SmallVec<[(ObjectId, SecondsSinceUnixEpoch); 2]>,
157}
158
159fn to_queue_key(i: i64, order: CommitTimeOrder) -> QueueKey<i64> {
160    match order {
161        CommitTimeOrder::NewestFirst => Ok(i),
162        CommitTimeOrder::OldestFirst => Err(Reverse(i)),
163    }
164}
165
166/// Compute the boundary at which the visible walk must stop because commits become reachable from
167/// both the visible tips and the hidden tips.
168///
169/// The algorithm performs a merge-base-style paint in a temporary `gix_revwalk::Graph`:
170/// visible tips are marked with `VISIBLE`, hidden tips with `HIDDEN`, and these flags are
171/// propagated to parents in generation/time order. Once a commit carries both flags, it is part
172/// of the overlap between the two histories and is marked `STALE` so older ancestors no longer
173/// need to keep re-propagating the same combined state.
174///
175/// The returned set is not all commits reachable from hidden tips. It is only the overlap frontier
176/// where the visible traversal must stop. The actual `Simple` walk then skips these commits and
177/// refuses to enqueue parents across them, which avoids traversing hidden-only history.
178fn compute_hidden_frontier(
179    visible_tips: &[ObjectId],
180    hidden_tips: &[ObjectId],
181    objects: &impl gix_object::Find,
182    cache: Option<&gix_commitgraph::Graph>,
183) -> Result<gix_revwalk::graph::IdMap<()>, Error> {
184    let mut graph = gix_revwalk::Graph::<gix_revwalk::graph::Commit<PaintFlags>>::new(objects, cache);
185    let mut queue = gix_revwalk::PriorityQueue::<GenThenTime, ObjectId>::new();
186
187    for &visible in visible_tips {
188        graph.get_or_insert_full_commit(visible, |commit| {
189            commit.data |= PaintFlags::VISIBLE;
190            queue.insert(GenThenTime::from(&*commit), visible);
191        })?;
192    }
193    for &hidden in hidden_tips {
194        graph.get_or_insert_full_commit(hidden, |commit| {
195            commit.data |= PaintFlags::HIDDEN;
196            queue.insert(GenThenTime::from(&*commit), hidden);
197        })?;
198    }
199
200    while queue.iter_unordered().any(|id| {
201        graph
202            .get(id)
203            .is_some_and(|commit| !commit.data.contains(PaintFlags::STALE))
204    }) {
205        let (_info, commit_id) = match queue.pop() {
206            Some(v) => v,
207            None => break,
208        };
209        let commit = graph.get_mut(&commit_id).expect("queued commits are in the graph");
210        let mut flags = commit.data;
211        if flags == (PaintFlags::VISIBLE | PaintFlags::HIDDEN) {
212            flags |= PaintFlags::STALE;
213        }
214
215        for parent_id in commit.parents.clone() {
216            graph.get_or_insert_full_commit(parent_id, |parent| {
217                if (parent.data & flags) != flags {
218                    parent.data |= flags;
219                    queue.insert(GenThenTime::from(&*parent), parent_id);
220                }
221            })?;
222        }
223    }
224
225    Ok(graph
226        .detach()
227        .into_iter()
228        .filter_map(|(id, commit)| {
229            commit
230                .data
231                .contains(PaintFlags::VISIBLE | PaintFlags::HIDDEN)
232                .then_some((id, ()))
233        })
234        .collect())
235}
236
237///
238mod init {
239    use super::{
240        collect_parents, compute_hidden_frontier, to_queue_key, CommitDateQueue, CommitTimeOrder, Error, Sorting, State,
241    };
242    use crate::commit::{Either, Info, ParentIds, Parents, Simple};
243    use gix_date::SecondsSinceUnixEpoch;
244    use gix_hash::{oid, ObjectId};
245    use gix_object::{CommitRefIter, FindExt};
246    use std::{cmp::Reverse, collections::VecDeque};
247
248    impl Default for State {
249        fn default() -> Self {
250            State {
251                next: Default::default(),
252                queue: gix_revwalk::PriorityQueue::new(),
253                buf: vec![],
254                object_hash: gix_hash::Kind::Sha1,
255                seen: Default::default(),
256                hidden: Default::default(),
257                hidden_tips: Vec::new(),
258                parents_buf: vec![],
259                parent_ids: Default::default(),
260            }
261        }
262    }
263
264    impl State {
265        fn clear(&mut self) {
266            let Self {
267                next,
268                queue,
269                buf,
270                object_hash,
271                seen,
272                hidden,
273                hidden_tips,
274                parents_buf: _,
275                parent_ids: _,
276            } = self;
277            next.clear();
278            queue.clear();
279            buf.clear();
280            *object_hash = gix_hash::Kind::Sha1;
281            seen.clear();
282            hidden.clear();
283            hidden_tips.clear();
284        }
285    }
286
287    impl Sorting {
288        /// If not topo sort, provide the cutoff date if present.
289        fn cutoff_time(&self) -> Option<SecondsSinceUnixEpoch> {
290            match self {
291                Sorting::ByCommitTimeCutoff { seconds, .. } => Some(*seconds),
292                _ => None,
293            }
294        }
295    }
296
297    /// Builder methods
298    impl<Find, Predicate> Simple<Find, Predicate>
299    where
300        Find: gix_object::Find,
301    {
302        /// Set the `sorting` method.
303        pub fn sorting(mut self, sorting: Sorting) -> Result<Self, Error> {
304            self.sorting = sorting;
305            match self.sorting {
306                Sorting::BreadthFirst => self.queue_to_vecdeque(),
307                Sorting::ByCommitTime(order) | Sorting::ByCommitTimeCutoff { order, .. } => {
308                    let state = &mut self.state;
309                    for commit_id in state.next.drain(..) {
310                        add_to_queue(
311                            commit_id,
312                            order,
313                            sorting.cutoff_time(),
314                            &mut state.queue,
315                            &self.objects,
316                            &mut state.buf,
317                        )?;
318                    }
319                }
320            }
321            Ok(self)
322        }
323
324        /// Change our commit parent handling mode to the given one.
325        pub fn parents(mut self, mode: Parents) -> Self {
326            self.parents = mode;
327            if matches!(self.parents, Parents::First) {
328                self.queue_to_vecdeque();
329            }
330            self
331        }
332
333        /// Hide the given `tips`, along with all commits reachable by them so that they will not be returned
334        /// by the traversal.
335        pub fn hide(mut self, tips: impl IntoIterator<Item = ObjectId>) -> Result<Self, Error> {
336            self.state.hidden_tips = tips.into_iter().collect();
337            Ok(self)
338        }
339
340        /// Set the commitgraph as `cache` to greatly accelerate any traversal.
341        ///
342        /// The cache will be used if possible, but we will fall back without error to using the object
343        /// database for commit lookup. If the cache is corrupt, we will fall back to the object database as well.
344        pub fn commit_graph(mut self, cache: Option<gix_commitgraph::Graph>) -> Self {
345            self.cache = cache;
346            self
347        }
348
349        fn queue_to_vecdeque(&mut self) {
350            let state = &mut self.state;
351            state.next.extend(
352                std::mem::replace(&mut state.queue, gix_revwalk::PriorityQueue::new())
353                    .into_iter_unordered()
354                    .map(|(_time, id)| id),
355            );
356        }
357
358        fn visible_inputs_sorted(&self) -> Vec<ObjectId> {
359            let mut out: Vec<_> = self
360                .state
361                .next
362                .iter()
363                .copied()
364                .chain(self.state.queue.iter_unordered().copied())
365                .collect();
366            out.sort();
367            out.dedup();
368            out
369        }
370
371        fn compute_hidden_frontier(&mut self, hidden_tips: Vec<ObjectId>) -> Result<(), Error> {
372            self.state.hidden.clear();
373            if hidden_tips.is_empty() {
374                return Ok(());
375            }
376            let visible_tips = self.visible_inputs_sorted();
377            if visible_tips.is_empty() {
378                return Ok(());
379            }
380            self.state.hidden =
381                compute_hidden_frontier(&visible_tips, &hidden_tips, &self.objects, self.cache.as_ref())?;
382            self.state.next.retain(|id| !self.state.hidden.contains_key(id));
383            self.state.queue = std::mem::replace(&mut self.state.queue, gix_revwalk::PriorityQueue::new())
384                .into_iter_unordered()
385                .filter(|(_, id)| !self.state.hidden.contains_key(id))
386                .collect();
387            Ok(())
388        }
389    }
390
391    fn add_to_queue(
392        commit_id: ObjectId,
393        order: CommitTimeOrder,
394        cutoff_time: Option<SecondsSinceUnixEpoch>,
395        queue: &mut CommitDateQueue,
396        objects: &impl gix_object::Find,
397        buf: &mut Vec<u8>,
398    ) -> Result<(), Error> {
399        let commit_iter = objects.find_commit_iter(&commit_id, buf)?;
400        let time = commit_iter.committer()?.seconds();
401        let key = to_queue_key(time, order);
402        match (cutoff_time, order) {
403            (Some(cutoff_time), _) if time >= cutoff_time => queue.insert(key, commit_id),
404            (Some(_), _) => {}
405            (None, _) => queue.insert(key, commit_id),
406        }
407        Ok(())
408    }
409
410    /// Lifecycle methods
411    impl<Find> Simple<Find, fn(&oid) -> bool>
412    where
413        Find: gix_object::Find,
414    {
415        /// Create a new instance.
416        ///
417        /// * `find` - a way to lookup new object data during traversal by their `ObjectId`, writing their data into buffer and returning
418        ///   an iterator over commit tokens if the object is present and is a commit. Caching should be implemented within this function
419        ///   as needed.
420        /// * `tips`
421        ///   * the starting points of the iteration, usually commits
422        ///   * each commit they lead to will only be returned once, including the tip that started it
423        pub fn new(tips: impl IntoIterator<Item = impl Into<ObjectId>>, find: Find) -> Self {
424            Self::filtered(tips, find, |_| true)
425        }
426    }
427
428    impl<Find, Predicate> Simple<Find, Predicate>
429    where
430        Find: gix_object::Find,
431        Predicate: FnMut(&oid) -> bool,
432    {
433        /// Create a new instance with commit filtering enabled.
434        ///
435        /// * `find` - a way to lookup new object data during traversal by their `ObjectId`, writing their data into buffer and returning
436        ///   an iterator over commit tokens if the object is present and is a commit. Caching should be implemented within this function
437        ///   as needed.
438        /// * `tips`
439        ///   * the starting points of the iteration, usually commits
440        ///   * each commit they lead to will only be returned once, including the tip that started it
441        /// * `predicate` - indicate whether a given commit should be included in the result as well
442        ///   as whether its parent commits should be traversed.
443        pub fn filtered(
444            tips: impl IntoIterator<Item = impl Into<ObjectId>>,
445            find: Find,
446            mut predicate: Predicate,
447        ) -> Self {
448            let tips = tips.into_iter();
449            let mut state = State::default();
450            {
451                state.clear();
452                state.next.reserve(tips.size_hint().0);
453                for tip in tips.map(Into::into) {
454                    if state.seen.insert(tip) && predicate(&tip) {
455                        state.next.push_back(tip);
456                    }
457                }
458            }
459            Self {
460                objects: find,
461                cache: None,
462                predicate,
463                state,
464                parents: Default::default(),
465                sorting: Default::default(),
466            }
467        }
468    }
469
470    /// Access
471    impl<Find, Predicate> Simple<Find, Predicate> {
472        /// Return an iterator for accessing data of the current commit, parsed lazily.
473        pub fn commit_iter(&self) -> CommitRefIter<'_> {
474            CommitRefIter::from_bytes(self.commit_data(), self.state.object_hash)
475        }
476
477        /// Return the current commits' raw data, which can be parsed using [`gix_object::CommitRef::from_bytes()`].
478        pub fn commit_data(&self) -> &[u8] {
479            &self.state.buf
480        }
481    }
482
483    impl<Find, Predicate> Iterator for Simple<Find, Predicate>
484    where
485        Find: gix_object::Find,
486        Predicate: FnMut(&oid) -> bool,
487    {
488        type Item = Result<Info, Error>;
489
490        fn next(&mut self) -> Option<Self::Item> {
491            if !self.state.hidden_tips.is_empty() {
492                let hidden_tips = std::mem::take(&mut self.state.hidden_tips);
493                if let Err(err) = self.compute_hidden_frontier(hidden_tips) {
494                    self.state.queue.clear();
495                    self.state.next.clear();
496                    return Some(Err(err));
497                }
498            }
499            if matches!(self.parents, Parents::First) {
500                self.next_by_topology()
501            } else {
502                match self.sorting {
503                    Sorting::BreadthFirst => self.next_by_topology(),
504                    Sorting::ByCommitTime(order) => self.next_by_commit_date(order, None),
505                    Sorting::ByCommitTimeCutoff { seconds, order } => self.next_by_commit_date(order, seconds.into()),
506                }
507            }
508        }
509    }
510
511    /// Utilities
512    impl<Find, Predicate> Simple<Find, Predicate>
513    where
514        Find: gix_object::Find,
515        Predicate: FnMut(&oid) -> bool,
516    {
517        fn next_by_commit_date(
518            &mut self,
519            order: CommitTimeOrder,
520            cutoff: Option<SecondsSinceUnixEpoch>,
521        ) -> Option<Result<Info, Error>> {
522            let state = &mut self.state;
523            let next = &mut state.queue;
524
525            loop {
526                let (commit_time, oid) = match next.pop()? {
527                    (Ok(t) | Err(Reverse(t)), o) => (t, o),
528                };
529                state.object_hash = oid.kind();
530                if state.hidden.contains_key(&oid) {
531                    continue;
532                }
533                let mut parents: ParentIds = Default::default();
534
535                match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) {
536                    Ok(Either::CachedCommit(commit)) => {
537                        if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) {
538                            // drop corrupt caches and try again with ODB
539                            self.cache = None;
540                            return self.next_by_commit_date(order, cutoff);
541                        }
542                        for (id, parent_commit_time) in state.parent_ids.drain(..) {
543                            parents.push(id);
544                            insert_into_seen_and_queue(
545                                &mut state.seen,
546                                &state.hidden,
547                                id,
548                                &mut self.predicate,
549                                next,
550                                order,
551                                cutoff,
552                                || parent_commit_time,
553                            );
554                        }
555                    }
556                    Ok(Either::CommitRefIter(commit_iter)) => {
557                        for token in commit_iter {
558                            match token {
559                                Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue,
560                                Ok(gix_object::commit::ref_iter::Token::Parent { id }) => {
561                                    parents.push(id);
562                                    insert_into_seen_and_queue(
563                                        &mut state.seen,
564                                        &state.hidden,
565                                        id,
566                                        &mut self.predicate,
567                                        next,
568                                        order,
569                                        cutoff,
570                                        || {
571                                            let parent =
572                                                self.objects.find_commit_iter(id.as_ref(), &mut state.parents_buf).ok();
573                                            parent
574                                                .and_then(|parent| {
575                                                    parent.committer().ok().map(|committer| committer.seconds())
576                                                })
577                                                .unwrap_or_default()
578                                        },
579                                    );
580                                }
581                                Ok(_unused_token) => break,
582                                Err(err) => return Some(Err(err.into())),
583                            }
584                        }
585                    }
586                    Err(err) => return Some(Err(err.into())),
587                }
588
589                return Some(Ok(Info {
590                    id: oid,
591                    parent_ids: parents,
592                    commit_time: Some(commit_time),
593                }));
594            }
595        }
596
597        fn next_by_topology(&mut self) -> Option<Result<Info, Error>> {
598            let state = &mut self.state;
599            let next = &mut state.next;
600
601            loop {
602                let oid = next.pop_front()?;
603                state.object_hash = oid.kind();
604                if state.hidden.contains_key(&oid) {
605                    continue;
606                }
607                let mut parents: ParentIds = Default::default();
608
609                match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) {
610                    Ok(Either::CachedCommit(commit)) => {
611                        if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) {
612                            // drop corrupt caches and try again with ODB
613                            self.cache = None;
614                            return self.next_by_topology();
615                        }
616
617                        for (pid, _commit_time) in state.parent_ids.drain(..) {
618                            parents.push(pid);
619                            insert_into_seen_and_next(&mut state.seen, &state.hidden, pid, &mut self.predicate, next);
620                            if matches!(self.parents, Parents::First) {
621                                break;
622                            }
623                        }
624                    }
625                    Ok(Either::CommitRefIter(commit_iter)) => {
626                        for token in commit_iter {
627                            match token {
628                                Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue,
629                                Ok(gix_object::commit::ref_iter::Token::Parent { id: pid }) => {
630                                    parents.push(pid);
631                                    insert_into_seen_and_next(
632                                        &mut state.seen,
633                                        &state.hidden,
634                                        pid,
635                                        &mut self.predicate,
636                                        next,
637                                    );
638                                    if matches!(self.parents, Parents::First) {
639                                        break;
640                                    }
641                                }
642                                Ok(_a_token_past_the_parents) => break,
643                                Err(err) => return Some(Err(err.into())),
644                            }
645                        }
646                    }
647                    Err(err) => return Some(Err(err.into())),
648                }
649
650                return Some(Ok(Info {
651                    id: oid,
652                    parent_ids: parents,
653                    commit_time: None,
654                }));
655            }
656        }
657    }
658
659    fn insert_into_seen_and_next(
660        seen: &mut gix_hashtable::HashSet<ObjectId>,
661        hidden: &gix_revwalk::graph::IdMap<()>,
662        parent_id: ObjectId,
663        predicate: &mut impl FnMut(&oid) -> bool,
664        next: &mut VecDeque<ObjectId>,
665    ) {
666        if hidden.contains_key(&parent_id) {
667            return;
668        }
669        if seen.insert(parent_id) && predicate(&parent_id) {
670            next.push_back(parent_id);
671        }
672    }
673
674    #[allow(clippy::too_many_arguments)]
675    fn insert_into_seen_and_queue(
676        seen: &mut gix_hashtable::HashSet<ObjectId>,
677        hidden: &gix_revwalk::graph::IdMap<()>,
678        parent_id: ObjectId,
679        predicate: &mut impl FnMut(&oid) -> bool,
680        queue: &mut CommitDateQueue,
681        order: CommitTimeOrder,
682        cutoff: Option<SecondsSinceUnixEpoch>,
683        get_parent_commit_time: impl FnOnce() -> gix_date::SecondsSinceUnixEpoch,
684    ) {
685        if hidden.contains_key(&parent_id) {
686            return;
687        }
688        if seen.insert(parent_id) && predicate(&parent_id) {
689            let parent_commit_time = get_parent_commit_time();
690            let key = to_queue_key(parent_commit_time, order);
691            match cutoff {
692                Some(cutoff_older_than) if parent_commit_time < cutoff_older_than => {}
693                Some(_) | None => queue.insert(key, parent_id),
694            }
695        }
696    }
697}
698
699fn collect_parents(
700    dest: &mut SmallVec<[(gix_hash::ObjectId, gix_date::SecondsSinceUnixEpoch); 2]>,
701    cache: Option<&gix_commitgraph::Graph>,
702    parents: gix_commitgraph::file::commit::Parents<'_>,
703) -> bool {
704    dest.clear();
705    let cache = cache.as_ref().expect("parents iter is available, backed by `cache`");
706    for parent_id in parents {
707        match parent_id {
708            Ok(pos) => dest.push({
709                let parent = cache.commit_at(pos);
710                (
711                    parent.id().to_owned(),
712                    parent.committer_timestamp() as gix_date::SecondsSinceUnixEpoch,
713                )
714            }),
715            Err(_err) => return false,
716        }
717    }
718    true
719}