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)]
8pub enum CommitTimeOrder {
10 #[default]
11 NewestFirst,
13 #[doc(alias = "Sort::REVERSE", alias = "git2")]
15 OldestFirst,
16}
17
18#[derive(Default, Debug, Copy, Clone)]
31pub enum Sorting {
32 #[default]
41 BreadthFirst,
42 ByCommitTime(CommitTimeOrder),
54 ByCommitTimeCutoff {
61 order: CommitTimeOrder,
63 seconds: gix_date::SecondsSinceUnixEpoch,
65 },
66}
67
68#[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#[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 candidates: Option<Candidates>,
100}
101
102#[derive(Debug, Clone, Copy)]
103enum CommitState {
104 Interesting,
106 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
119mod 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 impl<Find, Predicate> Simple<Find, Predicate>
177 where
178 Find: gix_object::Find,
179 {
180 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 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 pub fn hide(mut self, tips: impl IntoIterator<Item = ObjectId>) -> Result<Self, Error> {
225 let hidden_tips: Vec<ObjectId> = tips.into_iter().collect();
227 if hidden_tips.is_empty() {
228 return Ok(self);
229 }
230
231 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 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 self.cache = None;
250 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 self.state.candidates = None;
288
289 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 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 impl<Find> Simple<Find, fn(&oid) -> bool>
342 where
343 Find: gix_object::Find,
344 {
345 pub fn new(tips: impl IntoIterator<Item = impl Into<ObjectId>>, find: Find) -> Self {
354 Self::filtered(tips, find, |_| true)
355 }
356 }
357
358 impl<Find, Predicate> Simple<Find, Predicate>
360 where
361 Find: gix_object::Find,
362 Predicate: FnMut(&oid) -> bool,
363 {
364 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 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 impl<Find, Predicate> Simple<Find, Predicate> {
406 pub fn commit_iter(&self) -> CommitRefIter<'_> {
408 CommitRefIter::from_bytes(self.commit_data())
409 }
410
411 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 fn cutoff_time(&self) -> Option<SecondsSinceUnixEpoch> {
446 match self {
447 Sorting::ByCommitTimeCutoff { seconds, .. } => Some(*seconds),
448 _ => None,
449 }
450 }
451 }
452
453 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 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 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 candidates.push_back(info);
550 }
551 }
552 }
553 CommitState::Hidden => continue 'skip_hidden,
554 }
555 }
556 }
557 }
558
559 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 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 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 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 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 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, )
786 }),
787 Err(_err) => return false,
788 }
789 }
790 true
791}