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>,
98}
99
100#[derive(Debug, Clone, Copy)]
101enum CommitState {
102 Interesting,
104 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
117mod 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 impl<Find, Predicate> Simple<Find, Predicate>
175 where
176 Find: gix_object::Find,
177 {
178 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 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 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 previous.is_none() {
227 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 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 impl<Find> Simple<Find, fn(&oid) -> bool>
302 where
303 Find: gix_object::Find,
304 {
305 pub fn new(tips: impl IntoIterator<Item = impl Into<ObjectId>>, find: Find) -> Self {
314 Self::filtered(tips, find, |_| true)
315 }
316 }
317
318 impl<Find, Predicate> Simple<Find, Predicate>
320 where
321 Find: gix_object::Find,
322 Predicate: FnMut(&oid) -> bool,
323 {
324 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 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 impl<Find, Predicate> Simple<Find, Predicate> {
366 pub fn commit_iter(&self) -> CommitRefIter<'_> {
368 CommitRefIter::from_bytes(self.commit_data())
369 }
370
371 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 fn cutoff_time(&self) -> Option<SecondsSinceUnixEpoch> {
406 match self {
407 Sorting::ByCommitTimeCutoff { seconds, .. } => Some(*seconds),
408 _ => None,
409 }
410 }
411 }
412
413 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 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 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 candidates.push_back(info);
510 }
511 }
512 }
513 CommitState::Hidden => continue 'skip_hidden,
514 }
515 }
516 }
517 }
518
519 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 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 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 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 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 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, )
746 }),
747 Err(_err) => return false,
748 }
749 }
750 true
751}