Skip to main content

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    // TODO(perf): review this as we don't really need candidates anymore, given our current way of doing things.
98    //             However, maybe they can see use when getting an incremental traversal done.
99    candidates: Option<Candidates>,
100}
101
102#[derive(Debug, Clone, Copy)]
103enum CommitState {
104    /// The commit may be returned, it hasn't been hidden yet.
105    Interesting,
106    /// The commit should not be returned.
107    Hidden,
108}
109
110impl CommitState {
111    pub fn is_hidden(&self) -> bool {
112        matches!(self, CommitState::Hidden)
113    }
114    pub fn is_interesting(&self) -> bool {
115        matches!(self, CommitState::Interesting)
116    }
117}
118
119///
120mod init {
121    use std::cmp::Reverse;
122
123    use super::{
124        super::{simple::Sorting, Either, Info, ParentIds, Parents, Simple},
125        collect_parents, Candidates, CommitDateQueue, CommitState, CommitTimeOrder, Error, State,
126    };
127    use gix_date::SecondsSinceUnixEpoch;
128    use gix_hash::{oid, ObjectId};
129    use gix_hashtable::hash_map::Entry;
130    use gix_object::{CommitRefIter, FindExt};
131    use std::collections::VecDeque;
132    use Err as Oldest;
133    use Ok as Newest;
134
135    impl Default for State {
136        fn default() -> Self {
137            State {
138                next: Default::default(),
139                queue: gix_revwalk::PriorityQueue::new(),
140                buf: vec![],
141                seen: Default::default(),
142                parents_buf: vec![],
143                parent_ids: Default::default(),
144                candidates: None,
145            }
146        }
147    }
148
149    impl State {
150        fn clear(&mut self) {
151            let Self {
152                next,
153                queue,
154                buf,
155                seen,
156                parents_buf: _,
157                parent_ids: _,
158                candidates,
159            } = self;
160            next.clear();
161            queue.clear();
162            buf.clear();
163            seen.clear();
164            *candidates = None;
165        }
166    }
167
168    fn to_queue_key(i: i64, order: CommitTimeOrder) -> super::QueueKey<i64> {
169        match order {
170            CommitTimeOrder::NewestFirst => Newest(i),
171            CommitTimeOrder::OldestFirst => Oldest(Reverse(i)),
172        }
173    }
174
175    /// Builder
176    impl<Find, Predicate> Simple<Find, Predicate>
177    where
178        Find: gix_object::Find,
179    {
180        /// Set the `sorting` method.
181        pub fn sorting(mut self, sorting: Sorting) -> Result<Self, Error> {
182            self.sorting = sorting;
183            match self.sorting {
184                Sorting::BreadthFirst => {
185                    self.queue_to_vecdeque();
186                }
187                Sorting::ByCommitTime(order) | Sorting::ByCommitTimeCutoff { order, .. } => {
188                    let state = &mut self.state;
189                    for (commit_id, commit_state) in state.next.drain(..) {
190                        add_to_queue(
191                            commit_id,
192                            commit_state,
193                            order,
194                            sorting.cutoff_time(),
195                            &mut state.queue,
196                            &self.objects,
197                            &mut state.buf,
198                        )?;
199                    }
200                }
201            }
202            Ok(self)
203        }
204
205        /// Change our commit parent handling mode to the given one.
206        pub fn parents(mut self, mode: Parents) -> Self {
207            self.parents = mode;
208            if matches!(self.parents, Parents::First) {
209                self.queue_to_vecdeque();
210            }
211            self
212        }
213
214        /// Hide the given `tips`, along with all commits reachable by them so that they will not be returned
215        /// by the traversal.
216        ///
217        /// This function fully traverses all hidden tips and their ancestors, marking them as hidden
218        /// before iteration begins. This approach ensures correct behavior regardless
219        /// of graph topology or traversal order, matching git's `rev-list --not` behavior,
220        /// at great cost to performance, unfortunately.
221        ///
222        /// Note that hidden objects are expected to exist.
223        // TODO(perf): make this hiding iterative to avoid traversing the entire graph, always.
224        pub fn hide(mut self, tips: impl IntoIterator<Item = ObjectId>) -> Result<Self, Error> {
225            // Collect hidden tips first
226            let hidden_tips: Vec<ObjectId> = tips.into_iter().collect();
227            if hidden_tips.is_empty() {
228                return Ok(self);
229            }
230
231            // Fully traverse all hidden tips and mark all reachable commits as Hidden.
232            // This is "graph painting" - we paint all hidden commits upfront rather than
233            // interleaving hidden and interesting traversals, which ensures correct behavior
234            // regardless of graph topology or traversal order.
235            let mut queue: VecDeque<ObjectId> = VecDeque::new();
236
237            for id_to_ignore in hidden_tips {
238                if self.state.seen.insert(id_to_ignore, CommitState::Hidden).is_none() {
239                    queue.push_back(id_to_ignore);
240                }
241            }
242
243            // Process all hidden commits and their ancestors
244            while let Some(id) = queue.pop_front() {
245                match super::super::find(self.cache.as_ref(), &self.objects, &id, &mut self.state.buf) {
246                    Ok(Either::CachedCommit(commit)) => {
247                        if !collect_parents(&mut self.state.parent_ids, self.cache.as_ref(), commit.iter_parents()) {
248                            // drop corrupt caches and retry
249                            self.cache = None;
250                            // Re-add to queue to retry without cache
251                            if self.state.seen.get(&id).is_some_and(CommitState::is_hidden) {
252                                queue.push_back(id);
253                            }
254                            continue;
255                        }
256                        for (parent_id, _commit_time) in self.state.parent_ids.drain(..) {
257                            if self.state.seen.insert(parent_id, CommitState::Hidden).is_none() {
258                                queue.push_back(parent_id);
259                            }
260                        }
261                    }
262                    Ok(Either::CommitRefIter(commit_iter)) => {
263                        for token in commit_iter {
264                            match token {
265                                Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue,
266                                Ok(gix_object::commit::ref_iter::Token::Parent { id: parent_id }) => {
267                                    if self.state.seen.insert(parent_id, CommitState::Hidden).is_none() {
268                                        queue.push_back(parent_id);
269                                    }
270                                }
271                                Ok(_unused_token) => break,
272                                Err(err) => return Err(err.into()),
273                            }
274                        }
275                    }
276                    Err(err) => return Err(err.into()),
277                }
278            }
279
280            // Now that all hidden commits are painted, we no longer need special handling
281            // during the main traversal. We can remove hidden commits from the main queues
282            // and simply skip them during iteration.
283            //
284            // Note: We don't need the candidates buffer anymore since hidden commits are
285            // pre-painted. But we keep it for compatibility with existing behavior and
286            // in case interesting commits were already queued before hide() was called.
287            self.state.candidates = None;
288
289            // Remove any hidden commits from the interesting queues
290            self.state
291                .next
292                .retain(|(id, _)| !self.state.seen.get(id).is_some_and(CommitState::is_hidden));
293
294            Ok(self)
295        }
296
297        /// Set the commitgraph as `cache` to greatly accelerate any traversal.
298        ///
299        /// The cache will be used if possible, but we will fall back without error to using the object
300        /// database for commit lookup. If the cache is corrupt, we will fall back to the object database as well.
301        pub fn commit_graph(mut self, cache: Option<gix_commitgraph::Graph>) -> Self {
302            self.cache = cache;
303            self
304        }
305
306        fn queue_to_vecdeque(&mut self) {
307            let state = &mut self.state;
308            state.next.extend(
309                std::mem::replace(&mut state.queue, gix_revwalk::PriorityQueue::new())
310                    .into_iter_unordered()
311                    .map(|(_time, id)| id),
312            );
313        }
314    }
315
316    fn add_to_queue(
317        commit_id: ObjectId,
318        commit_state: CommitState,
319        order: CommitTimeOrder,
320        cutoff_time: Option<SecondsSinceUnixEpoch>,
321        queue: &mut CommitDateQueue,
322        objects: &impl gix_object::Find,
323        buf: &mut Vec<u8>,
324    ) -> Result<(), Error> {
325        let commit_iter = objects.find_commit_iter(&commit_id, buf)?;
326        let time = commit_iter.committer()?.seconds();
327        let key = to_queue_key(time, order);
328        match (cutoff_time, order) {
329            (Some(cutoff_time), _) if time >= cutoff_time => {
330                queue.insert(key, (commit_id, commit_state));
331            }
332            (Some(_), _) => {}
333            (None, _) => {
334                queue.insert(key, (commit_id, commit_state));
335            }
336        }
337        Ok(())
338    }
339
340    /// Lifecycle
341    impl<Find> Simple<Find, fn(&oid) -> bool>
342    where
343        Find: gix_object::Find,
344    {
345        /// Create a new instance.
346        ///
347        /// * `find` - a way to lookup new object data during traversal by their `ObjectId`, writing their data into buffer and returning
348        ///   an iterator over commit tokens if the object is present and is a commit. Caching should be implemented within this function
349        ///   as needed.
350        /// * `tips`
351        ///   * the starting points of the iteration, usually commits
352        ///   * each commit they lead to will only be returned once, including the tip that started it
353        pub fn new(tips: impl IntoIterator<Item = impl Into<ObjectId>>, find: Find) -> Self {
354            Self::filtered(tips, find, |_| true)
355        }
356    }
357
358    /// Lifecycle
359    impl<Find, Predicate> Simple<Find, Predicate>
360    where
361        Find: gix_object::Find,
362        Predicate: FnMut(&oid) -> bool,
363    {
364        /// Create a new instance with commit filtering enabled.
365        ///
366        /// * `find` - a way to lookup new object data during traversal by their `ObjectId`, writing their data into buffer and returning
367        ///   an iterator over commit tokens if the object is present and is a commit. Caching should be implemented within this function
368        ///   as needed.
369        /// * `tips`
370        ///   * the starting points of the iteration, usually commits
371        ///   * each commit they lead to will only be returned once, including the tip that started it
372        /// * `predicate` - indicate whether a given commit should be included in the result as well
373        ///   as whether its parent commits should be traversed.
374        pub fn filtered(
375            tips: impl IntoIterator<Item = impl Into<ObjectId>>,
376            find: Find,
377            mut predicate: Predicate,
378        ) -> Self {
379            let tips = tips.into_iter();
380            let mut state = State::default();
381            {
382                state.clear();
383                state.next.reserve(tips.size_hint().0);
384                for tip in tips.map(Into::into) {
385                    let commit_state = CommitState::Interesting;
386                    let seen = state.seen.insert(tip, commit_state);
387                    // We know there can only be duplicate interesting ones.
388                    if seen.is_none() && predicate(&tip) {
389                        state.next.push_back((tip, commit_state));
390                    }
391                }
392            }
393            Self {
394                objects: find,
395                cache: None,
396                predicate,
397                state,
398                parents: Default::default(),
399                sorting: Default::default(),
400            }
401        }
402    }
403
404    /// Access
405    impl<Find, Predicate> Simple<Find, Predicate> {
406        /// Return an iterator for accessing data of the current commit, parsed lazily.
407        pub fn commit_iter(&self) -> CommitRefIter<'_> {
408            CommitRefIter::from_bytes(self.commit_data())
409        }
410
411        /// Return the current commits' raw data, which can be parsed using [`gix_object::CommitRef::from_bytes()`].
412        pub fn commit_data(&self) -> &[u8] {
413            &self.state.buf
414        }
415    }
416
417    impl<Find, Predicate> Iterator for Simple<Find, Predicate>
418    where
419        Find: gix_object::Find,
420        Predicate: FnMut(&oid) -> bool,
421    {
422        type Item = Result<Info, Error>;
423
424        fn next(&mut self) -> Option<Self::Item> {
425            if matches!(self.parents, Parents::First) {
426                self.next_by_topology()
427            } else {
428                match self.sorting {
429                    Sorting::BreadthFirst => self.next_by_topology(),
430                    Sorting::ByCommitTime(order) => self.next_by_commit_date(order, None),
431                    Sorting::ByCommitTimeCutoff { seconds, order } => self.next_by_commit_date(order, seconds.into()),
432                }
433            }
434            .or_else(|| {
435                self.state
436                    .candidates
437                    .as_mut()
438                    .and_then(|candidates| candidates.pop_front().map(Ok))
439            })
440        }
441    }
442
443    impl Sorting {
444        /// If not topo sort, provide the cutoff date if present.
445        fn cutoff_time(&self) -> Option<SecondsSinceUnixEpoch> {
446            match self {
447                Sorting::ByCommitTimeCutoff { seconds, .. } => Some(*seconds),
448                _ => None,
449            }
450        }
451    }
452
453    /// Utilities
454    impl<Find, Predicate> Simple<Find, Predicate>
455    where
456        Find: gix_object::Find,
457        Predicate: FnMut(&oid) -> bool,
458    {
459        fn next_by_commit_date(
460            &mut self,
461            order: CommitTimeOrder,
462            cutoff: Option<SecondsSinceUnixEpoch>,
463        ) -> Option<Result<Info, Error>> {
464            let state = &mut self.state;
465            let next = &mut state.queue;
466
467            'skip_hidden: loop {
468                let (commit_time, (oid, _queued_commit_state)) = match next.pop()? {
469                    (Newest(t) | Oldest(Reverse(t)), o) => (t, o),
470                };
471                let mut parents: ParentIds = Default::default();
472
473                // Always use the state that is actually stored, as we may change the type as we go.
474                let commit_state = *state.seen.get(&oid).expect("every commit we traverse has state added");
475                if can_deplete_candidates_early(
476                    next.iter_unordered().map(|t| t.1),
477                    commit_state,
478                    state.candidates.as_ref(),
479                ) {
480                    return None;
481                }
482                match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) {
483                    Ok(Either::CachedCommit(commit)) => {
484                        if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) {
485                            // drop corrupt caches and try again with ODB
486                            self.cache = None;
487                            return self.next_by_commit_date(order, cutoff);
488                        }
489                        for (id, parent_commit_time) in state.parent_ids.drain(..) {
490                            parents.push(id);
491                            insert_into_seen_and_queue(
492                                &mut state.seen,
493                                id,
494                                &mut state.candidates,
495                                commit_state,
496                                &mut self.predicate,
497                                next,
498                                order,
499                                cutoff,
500                                || parent_commit_time,
501                            );
502                        }
503                    }
504                    Ok(Either::CommitRefIter(commit_iter)) => {
505                        for token in commit_iter {
506                            match token {
507                                Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue,
508                                Ok(gix_object::commit::ref_iter::Token::Parent { id }) => {
509                                    parents.push(id);
510                                    insert_into_seen_and_queue(
511                                        &mut state.seen,
512                                        id,
513                                        &mut state.candidates,
514                                        commit_state,
515                                        &mut self.predicate,
516                                        next,
517                                        order,
518                                        cutoff,
519                                        || {
520                                            let parent =
521                                                self.objects.find_commit_iter(id.as_ref(), &mut state.parents_buf).ok();
522                                            parent
523                                                .and_then(|parent| {
524                                                    parent.committer().ok().map(|committer| committer.seconds())
525                                                })
526                                                .unwrap_or_default()
527                                        },
528                                    );
529                                }
530                                Ok(_unused_token) => break,
531                                Err(err) => return Some(Err(err.into())),
532                            }
533                        }
534                    }
535                    Err(err) => return Some(Err(err.into())),
536                }
537                match commit_state {
538                    CommitState::Interesting => {
539                        let info = Info {
540                            id: oid,
541                            parent_ids: parents,
542                            commit_time: Some(commit_time),
543                        };
544                        match state.candidates.as_mut() {
545                            None => return Some(Ok(info)),
546                            Some(candidates) => {
547                                // assure candidates aren't prematurely returned - hidden commits may catch up with
548                                // them later.
549                                candidates.push_back(info);
550                            }
551                        }
552                    }
553                    CommitState::Hidden => continue 'skip_hidden,
554                }
555            }
556        }
557    }
558
559    /// Returns `true` if we have only hidden cursors queued for traversal, assuming that we don't see interesting ones ever again.
560    ///
561    /// `unqueued_commit_state` is the state of the commit that is currently being processed.
562    fn can_deplete_candidates_early(
563        mut queued_states: impl Iterator<Item = CommitState>,
564        unqueued_commit_state: CommitState,
565        candidates: Option<&Candidates>,
566    ) -> bool {
567        if candidates.is_none() {
568            return false;
569        }
570        if unqueued_commit_state.is_interesting() {
571            return false;
572        }
573
574        let mut is_empty = true;
575        queued_states.all(|state| {
576            is_empty = false;
577            state.is_hidden()
578        }) && !is_empty
579    }
580
581    /// Utilities
582    impl<Find, Predicate> Simple<Find, Predicate>
583    where
584        Find: gix_object::Find,
585        Predicate: FnMut(&oid) -> bool,
586    {
587        fn next_by_topology(&mut self) -> Option<Result<Info, Error>> {
588            let state = &mut self.state;
589            let next = &mut state.next;
590            'skip_hidden: loop {
591                let (oid, _queued_commit_state) = next.pop_front()?;
592                let mut parents: ParentIds = Default::default();
593
594                // Always use the state that is actually stored, as we may change the type as we go.
595                let commit_state = *state.seen.get(&oid).expect("every commit we traverse has state added");
596                if can_deplete_candidates_early(next.iter().map(|t| t.1), commit_state, state.candidates.as_ref()) {
597                    return None;
598                }
599                match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) {
600                    Ok(Either::CachedCommit(commit)) => {
601                        if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) {
602                            // drop corrupt caches and try again with ODB
603                            self.cache = None;
604                            return self.next_by_topology();
605                        }
606
607                        for (pid, _commit_time) in state.parent_ids.drain(..) {
608                            parents.push(pid);
609                            insert_into_seen_and_next(
610                                &mut state.seen,
611                                pid,
612                                &mut state.candidates,
613                                commit_state,
614                                &mut self.predicate,
615                                next,
616                            );
617                            if commit_state.is_interesting() && matches!(self.parents, Parents::First) {
618                                break;
619                            }
620                        }
621                    }
622                    Ok(Either::CommitRefIter(commit_iter)) => {
623                        for token in commit_iter {
624                            match token {
625                                Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue,
626                                Ok(gix_object::commit::ref_iter::Token::Parent { id: pid }) => {
627                                    parents.push(pid);
628                                    insert_into_seen_and_next(
629                                        &mut state.seen,
630                                        pid,
631                                        &mut state.candidates,
632                                        commit_state,
633                                        &mut self.predicate,
634                                        next,
635                                    );
636                                    if commit_state.is_interesting() && matches!(self.parents, Parents::First) {
637                                        break;
638                                    }
639                                }
640                                Ok(_a_token_past_the_parents) => break,
641                                Err(err) => return Some(Err(err.into())),
642                            }
643                        }
644                    }
645                    Err(err) => return Some(Err(err.into())),
646                }
647                match commit_state {
648                    CommitState::Interesting => {
649                        let info = Info {
650                            id: oid,
651                            parent_ids: parents,
652                            commit_time: None,
653                        };
654                        match state.candidates.as_mut() {
655                            None => return Some(Ok(info)),
656                            Some(candidates) => {
657                                // assure candidates aren't prematurely returned - hidden commits may catch up with
658                                // them later.
659                                candidates.push_back(info);
660                            }
661                        }
662                    }
663                    CommitState::Hidden => continue 'skip_hidden,
664                }
665            }
666        }
667    }
668
669    #[inline]
670    fn remove_candidate(candidates: Option<&mut Candidates>, remove: ObjectId) -> Option<()> {
671        let candidates = candidates?;
672        let pos = candidates
673            .iter_mut()
674            .enumerate()
675            .find_map(|(idx, info)| (info.id == remove).then_some(idx))?;
676        candidates.remove(pos);
677        None
678    }
679
680    fn insert_into_seen_and_next(
681        seen: &mut gix_revwalk::graph::IdMap<CommitState>,
682        parent_id: ObjectId,
683        candidates: &mut Option<Candidates>,
684        commit_state: CommitState,
685        predicate: &mut impl FnMut(&oid) -> bool,
686        next: &mut VecDeque<(ObjectId, CommitState)>,
687    ) {
688        let enqueue = match seen.entry(parent_id) {
689            Entry::Occupied(mut e) => {
690                let enqueue = handle_seen(commit_state, *e.get(), parent_id, candidates);
691                if commit_state.is_hidden() {
692                    e.insert(commit_state);
693                }
694                enqueue
695            }
696            Entry::Vacant(e) => {
697                e.insert(commit_state);
698                match commit_state {
699                    CommitState::Interesting => predicate(&parent_id),
700                    CommitState::Hidden => true,
701                }
702            }
703        };
704        if enqueue {
705            next.push_back((parent_id, commit_state));
706        }
707    }
708
709    #[allow(clippy::too_many_arguments)]
710    fn insert_into_seen_and_queue(
711        seen: &mut gix_revwalk::graph::IdMap<CommitState>,
712        parent_id: ObjectId,
713        candidates: &mut Option<Candidates>,
714        commit_state: CommitState,
715        predicate: &mut impl FnMut(&oid) -> bool,
716        queue: &mut CommitDateQueue,
717        order: CommitTimeOrder,
718        cutoff: Option<SecondsSinceUnixEpoch>,
719        get_parent_commit_time: impl FnOnce() -> gix_date::SecondsSinceUnixEpoch,
720    ) {
721        let enqueue = match seen.entry(parent_id) {
722            Entry::Occupied(mut e) => {
723                let enqueue = handle_seen(commit_state, *e.get(), parent_id, candidates);
724                if commit_state.is_hidden() {
725                    e.insert(commit_state);
726                }
727                enqueue
728            }
729            Entry::Vacant(e) => {
730                e.insert(commit_state);
731                match commit_state {
732                    CommitState::Interesting => (predicate)(&parent_id),
733                    CommitState::Hidden => true,
734                }
735            }
736        };
737
738        if enqueue {
739            let parent_commit_time = get_parent_commit_time();
740            let key = to_queue_key(parent_commit_time, order);
741            match cutoff {
742                Some(cutoff_older_than) if parent_commit_time < cutoff_older_than => {}
743                Some(_) | None => queue.insert(key, (parent_id, commit_state)),
744            }
745        }
746    }
747
748    #[inline]
749    #[must_use]
750    fn handle_seen(
751        next_state: CommitState,
752        current_state: CommitState,
753        id: ObjectId,
754        candidates: &mut Option<Candidates>,
755    ) -> bool {
756        match (current_state, next_state) {
757            (CommitState::Hidden, CommitState::Hidden) => false,
758            (CommitState::Interesting, CommitState::Interesting) => false,
759            (CommitState::Hidden, CommitState::Interesting) => {
760                // keep traversing to paint more hidden. After all, the commit_state overrides the current parent state
761                true
762            }
763            (CommitState::Interesting, CommitState::Hidden) => {
764                remove_candidate(candidates.as_mut(), id);
765                true
766            }
767        }
768    }
769}
770
771fn collect_parents(
772    dest: &mut SmallVec<[(gix_hash::ObjectId, gix_date::SecondsSinceUnixEpoch); 2]>,
773    cache: Option<&gix_commitgraph::Graph>,
774    parents: gix_commitgraph::file::commit::Parents<'_>,
775) -> bool {
776    dest.clear();
777    let cache = cache.as_ref().expect("parents iter is available, backed by `cache`");
778    for parent_id in parents {
779        match parent_id {
780            Ok(pos) => dest.push({
781                let parent = cache.commit_at(pos);
782                (
783                    parent.id().to_owned(),
784                    parent.committer_timestamp() as gix_date::SecondsSinceUnixEpoch, // we can't handle errors here and trying seems overkill
785                )
786            }),
787            Err(_err) => return false,
788        }
789    }
790    true
791}