gix_traverse/commit/
simple.rs

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