1use std::{
2 cmp::{Ordering, Reverse},
3 collections::VecDeque,
4};
5
6use gix_date::SecondsSinceUnixEpoch;
7use gix_hash::ObjectId;
8use smallvec::SmallVec;
9
10#[derive(Default, Debug, Copy, Clone)]
11pub enum CommitTimeOrder {
13 #[default]
14 NewestFirst,
16 #[doc(alias = "Sort::REVERSE", alias = "git2")]
18 OldestFirst,
19}
20
21#[derive(Default, Debug, Copy, Clone)]
34pub enum Sorting {
35 #[default]
44 BreadthFirst,
45 ByCommitTime(CommitTimeOrder),
57 ByCommitTimeCutoff {
64 order: CommitTimeOrder,
66 seconds: gix_date::SecondsSinceUnixEpoch,
68 },
69}
70
71#[derive(Debug, thiserror::Error)]
73#[expect(missing_docs)]
74pub enum Error {
75 #[error(transparent)]
76 Find(#[from] gix_object::find::existing_iter::Error),
77 #[error(transparent)]
78 ObjectDecode(#[from] gix_object::decode::Error),
79 #[error(transparent)]
80 HiddenGraph(#[from] gix_revwalk::graph::get_or_insert_default::Error),
81}
82
83use Result as Either;
84
85type QueueKey<T> = Either<T, Reverse<T>>;
86type CommitDateQueue = gix_revwalk::PriorityQueue<QueueKey<SecondsSinceUnixEpoch>, ObjectId>;
87
88bitflags::bitflags! {
89 #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
90 struct PaintFlags: u8 {
91 const VISIBLE = 1 << 0;
92 const HIDDEN = 1 << 1;
93 const STALE = 1 << 2;
94 }
95}
96
97#[derive(Debug, Clone, Copy, Eq, PartialEq)]
100struct GenThenTime {
101 generation: gix_revwalk::graph::Generation,
102 time: SecondsSinceUnixEpoch,
103}
104
105impl From<&gix_revwalk::graph::Commit<PaintFlags>> for GenThenTime {
106 fn from(commit: &gix_revwalk::graph::Commit<PaintFlags>) -> Self {
107 GenThenTime {
108 generation: commit.generation.unwrap_or(gix_commitgraph::GENERATION_NUMBER_INFINITY),
109 time: commit.commit_time,
110 }
111 }
112}
113
114impl Ord for GenThenTime {
115 fn cmp(&self, other: &Self) -> Ordering {
116 self.generation.cmp(&other.generation).then(self.time.cmp(&other.time))
117 }
118}
119
120impl PartialOrd<Self> for GenThenTime {
121 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
122 Some(self.cmp(other))
123 }
124}
125
126#[derive(Clone)]
128pub(super) struct State {
129 next: VecDeque<ObjectId>,
134 queue: CommitDateQueue,
139 buf: Vec<u8>,
141 object_hash: gix_hash::Kind,
145 seen: gix_hashtable::HashSet<ObjectId>,
147 hidden: gix_revwalk::graph::IdMap<()>,
149 hidden_tips: Vec<ObjectId>,
153 parents_buf: Vec<u8>,
155 parent_ids: SmallVec<[(ObjectId, SecondsSinceUnixEpoch); 2]>,
157}
158
159fn to_queue_key(i: i64, order: CommitTimeOrder) -> QueueKey<i64> {
160 match order {
161 CommitTimeOrder::NewestFirst => Ok(i),
162 CommitTimeOrder::OldestFirst => Err(Reverse(i)),
163 }
164}
165
166fn compute_hidden_frontier(
179 visible_tips: &[ObjectId],
180 hidden_tips: &[ObjectId],
181 objects: &impl gix_object::Find,
182 cache: Option<&gix_commitgraph::Graph>,
183) -> Result<gix_revwalk::graph::IdMap<()>, Error> {
184 let mut graph = gix_revwalk::Graph::<gix_revwalk::graph::Commit<PaintFlags>>::new(objects, cache);
185 let mut queue = gix_revwalk::PriorityQueue::<GenThenTime, ObjectId>::new();
186
187 for &visible in visible_tips {
188 graph.get_or_insert_full_commit(visible, |commit| {
189 commit.data |= PaintFlags::VISIBLE;
190 queue.insert(GenThenTime::from(&*commit), visible);
191 })?;
192 }
193 for &hidden in hidden_tips {
194 graph.get_or_insert_full_commit(hidden, |commit| {
195 commit.data |= PaintFlags::HIDDEN;
196 queue.insert(GenThenTime::from(&*commit), hidden);
197 })?;
198 }
199
200 while queue.iter_unordered().any(|id| {
201 graph
202 .get(id)
203 .is_some_and(|commit| !commit.data.contains(PaintFlags::STALE))
204 }) {
205 let (_info, commit_id) = match queue.pop() {
206 Some(v) => v,
207 None => break,
208 };
209 let commit = graph.get_mut(&commit_id).expect("queued commits are in the graph");
210 let mut flags = commit.data;
211 if flags == (PaintFlags::VISIBLE | PaintFlags::HIDDEN) {
212 flags |= PaintFlags::STALE;
213 }
214
215 for parent_id in commit.parents.clone() {
216 graph.get_or_insert_full_commit(parent_id, |parent| {
217 if (parent.data & flags) != flags {
218 parent.data |= flags;
219 queue.insert(GenThenTime::from(&*parent), parent_id);
220 }
221 })?;
222 }
223 }
224
225 Ok(graph
226 .detach()
227 .into_iter()
228 .filter_map(|(id, commit)| {
229 commit
230 .data
231 .contains(PaintFlags::VISIBLE | PaintFlags::HIDDEN)
232 .then_some((id, ()))
233 })
234 .collect())
235}
236
237mod init {
239 use super::{
240 CommitDateQueue, CommitTimeOrder, Error, Sorting, State, collect_parents, compute_hidden_frontier, to_queue_key,
241 };
242 use crate::commit::{Either, Info, ParentIds, Parents, Simple};
243 use gix_date::SecondsSinceUnixEpoch;
244 use gix_hash::{ObjectId, oid};
245 use gix_object::{CommitRefIter, FindExt};
246 use std::{cmp::Reverse, collections::VecDeque};
247
248 impl Default for State {
249 fn default() -> Self {
250 State {
251 next: Default::default(),
252 queue: gix_revwalk::PriorityQueue::new(),
253 buf: vec![],
254 object_hash: gix_hash::Kind::shortest(),
255 seen: Default::default(),
256 hidden: Default::default(),
257 hidden_tips: Vec::new(),
258 parents_buf: vec![],
259 parent_ids: Default::default(),
260 }
261 }
262 }
263
264 impl State {
265 fn clear(&mut self) {
266 let Self {
267 next,
268 queue,
269 buf,
270 object_hash,
271 seen,
272 hidden,
273 hidden_tips,
274 parents_buf: _,
275 parent_ids: _,
276 } = self;
277 next.clear();
278 queue.clear();
279 buf.clear();
280 *object_hash = gix_hash::Kind::shortest();
281 seen.clear();
282 hidden.clear();
283 hidden_tips.clear();
284 }
285 }
286
287 impl Sorting {
288 fn cutoff_time(&self) -> Option<SecondsSinceUnixEpoch> {
290 match self {
291 Sorting::ByCommitTimeCutoff { seconds, .. } => Some(*seconds),
292 _ => None,
293 }
294 }
295 }
296
297 impl<Find, Predicate> Simple<Find, Predicate>
299 where
300 Find: gix_object::Find,
301 {
302 pub fn sorting(mut self, sorting: Sorting) -> Result<Self, Error> {
304 self.sorting = sorting;
305 match self.sorting {
306 Sorting::BreadthFirst => self.queue_to_vecdeque(),
307 Sorting::ByCommitTime(order) | Sorting::ByCommitTimeCutoff { order, .. } => {
308 let state = &mut self.state;
309 for commit_id in state.next.drain(..) {
310 add_to_queue(
311 commit_id,
312 order,
313 sorting.cutoff_time(),
314 &mut state.queue,
315 &self.objects,
316 &mut state.buf,
317 )?;
318 }
319 }
320 }
321 Ok(self)
322 }
323
324 pub fn parents(mut self, mode: Parents) -> Self {
326 self.parents = mode;
327 if matches!(self.parents, Parents::First) {
328 self.queue_to_vecdeque();
329 }
330 self
331 }
332
333 pub fn hide(mut self, tips: impl IntoIterator<Item = ObjectId>) -> Result<Self, Error> {
336 self.state.hidden_tips = tips.into_iter().collect();
337 Ok(self)
338 }
339
340 pub fn commit_graph(mut self, cache: Option<gix_commitgraph::Graph>) -> Self {
345 self.cache = cache;
346 self
347 }
348
349 fn queue_to_vecdeque(&mut self) {
350 let state = &mut self.state;
351 state.next.extend(
352 std::mem::replace(&mut state.queue, gix_revwalk::PriorityQueue::new())
353 .into_iter_unordered()
354 .map(|(_time, id)| id),
355 );
356 }
357
358 fn visible_inputs_sorted(&self) -> Vec<ObjectId> {
359 let mut out: Vec<_> = self
360 .state
361 .next
362 .iter()
363 .copied()
364 .chain(self.state.queue.iter_unordered().copied())
365 .collect();
366 out.sort();
367 out.dedup();
368 out
369 }
370
371 fn compute_hidden_frontier(&mut self, hidden_tips: Vec<ObjectId>) -> Result<(), Error> {
372 self.state.hidden.clear();
373 if hidden_tips.is_empty() {
374 return Ok(());
375 }
376 let visible_tips = self.visible_inputs_sorted();
377 if visible_tips.is_empty() {
378 return Ok(());
379 }
380 self.state.hidden =
381 compute_hidden_frontier(&visible_tips, &hidden_tips, &self.objects, self.cache.as_ref())?;
382 self.state.next.retain(|id| !self.state.hidden.contains_key(id));
383 self.state.queue = std::mem::replace(&mut self.state.queue, gix_revwalk::PriorityQueue::new())
384 .into_iter_unordered()
385 .filter(|(_, id)| !self.state.hidden.contains_key(id))
386 .collect();
387 Ok(())
388 }
389 }
390
391 fn add_to_queue(
392 commit_id: ObjectId,
393 order: CommitTimeOrder,
394 cutoff_time: Option<SecondsSinceUnixEpoch>,
395 queue: &mut CommitDateQueue,
396 objects: &impl gix_object::Find,
397 buf: &mut Vec<u8>,
398 ) -> Result<(), Error> {
399 let commit_iter = objects.find_commit_iter(&commit_id, buf)?;
400 let time = commit_iter.committer()?.seconds();
401 let key = to_queue_key(time, order);
402 match (cutoff_time, order) {
403 (Some(cutoff_time), _) if time >= cutoff_time => queue.insert(key, commit_id),
404 (Some(_), _) => {}
405 (None, _) => queue.insert(key, commit_id),
406 }
407 Ok(())
408 }
409
410 impl<Find> Simple<Find, fn(&oid) -> bool>
412 where
413 Find: gix_object::Find,
414 {
415 pub fn new(tips: impl IntoIterator<Item = impl Into<ObjectId>>, find: Find) -> Self {
424 Self::filtered(tips, find, |_| true)
425 }
426 }
427
428 impl<Find, Predicate> Simple<Find, Predicate>
429 where
430 Find: gix_object::Find,
431 Predicate: FnMut(&oid) -> bool,
432 {
433 pub fn filtered(
444 tips: impl IntoIterator<Item = impl Into<ObjectId>>,
445 find: Find,
446 mut predicate: Predicate,
447 ) -> Self {
448 let tips = tips.into_iter();
449 let mut state = State::default();
450 {
451 state.clear();
452 state.next.reserve(tips.size_hint().0);
453 for tip in tips.map(Into::into) {
454 if state.seen.insert(tip) && predicate(&tip) {
455 state.next.push_back(tip);
456 }
457 }
458 }
459 Self {
460 objects: find,
461 cache: None,
462 predicate,
463 state,
464 parents: Default::default(),
465 sorting: Default::default(),
466 }
467 }
468 }
469
470 impl<Find, Predicate> Simple<Find, Predicate> {
472 pub fn commit_iter(&self) -> CommitRefIter<'_> {
474 CommitRefIter::from_bytes(self.commit_data(), self.state.object_hash)
475 }
476
477 pub fn commit_data(&self) -> &[u8] {
479 &self.state.buf
480 }
481 }
482
483 impl<Find, Predicate> Iterator for Simple<Find, Predicate>
484 where
485 Find: gix_object::Find,
486 Predicate: FnMut(&oid) -> bool,
487 {
488 type Item = Result<Info, Error>;
489
490 fn next(&mut self) -> Option<Self::Item> {
491 if !self.state.hidden_tips.is_empty() {
492 let hidden_tips = std::mem::take(&mut self.state.hidden_tips);
493 if let Err(err) = self.compute_hidden_frontier(hidden_tips) {
494 self.state.queue.clear();
495 self.state.next.clear();
496 return Some(Err(err));
497 }
498 }
499 if matches!(self.parents, Parents::First) {
500 self.next_by_topology()
501 } else {
502 match self.sorting {
503 Sorting::BreadthFirst => self.next_by_topology(),
504 Sorting::ByCommitTime(order) => self.next_by_commit_date(order, None),
505 Sorting::ByCommitTimeCutoff { seconds, order } => self.next_by_commit_date(order, seconds.into()),
506 }
507 }
508 }
509 }
510
511 impl<Find, Predicate> Simple<Find, Predicate>
513 where
514 Find: gix_object::Find,
515 Predicate: FnMut(&oid) -> bool,
516 {
517 fn next_by_commit_date(
518 &mut self,
519 order: CommitTimeOrder,
520 cutoff: Option<SecondsSinceUnixEpoch>,
521 ) -> Option<Result<Info, Error>> {
522 let state = &mut self.state;
523 let next = &mut state.queue;
524
525 loop {
526 let (commit_time, oid) = match next.pop()? {
527 (Ok(t) | Err(Reverse(t)), o) => (t, o),
528 };
529 state.object_hash = oid.kind();
530 if state.hidden.contains_key(&oid) {
531 continue;
532 }
533 let mut parents: ParentIds = Default::default();
534 let generation;
535
536 match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) {
537 Ok(Either::CachedCommit(commit)) => {
538 generation = Some(commit.generation());
539 if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) {
540 self.cache = None;
542 return self.next_by_commit_date(order, cutoff);
543 }
544 for (id, parent_commit_time) in state.parent_ids.drain(..) {
545 parents.push(id);
546 insert_into_seen_and_queue(
547 &mut state.seen,
548 &state.hidden,
549 id,
550 &mut self.predicate,
551 next,
552 order,
553 cutoff,
554 || parent_commit_time,
555 );
556 }
557 }
558 Ok(Either::CommitRefIter(commit_iter)) => {
559 generation = None;
560 for token in commit_iter {
561 match token {
562 Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue,
563 Ok(gix_object::commit::ref_iter::Token::Parent { id }) => {
564 parents.push(id);
565 insert_into_seen_and_queue(
566 &mut state.seen,
567 &state.hidden,
568 id,
569 &mut self.predicate,
570 next,
571 order,
572 cutoff,
573 || {
574 let parent =
575 self.objects.find_commit_iter(id.as_ref(), &mut state.parents_buf).ok();
576 parent
577 .and_then(|parent| {
578 parent.committer().ok().map(|committer| committer.seconds())
579 })
580 .unwrap_or_default()
581 },
582 );
583 }
584 Ok(_unused_token) => break,
585 Err(err) => return Some(Err(err.into())),
586 }
587 }
588 }
589 Err(err) => return Some(Err(err.into())),
590 }
591
592 return Some(Ok(Info {
593 id: oid,
594 parent_ids: parents,
595 generation,
596 commit_time: Some(commit_time),
597 }));
598 }
599 }
600
601 fn next_by_topology(&mut self) -> Option<Result<Info, Error>> {
602 let state = &mut self.state;
603 let next = &mut state.next;
604
605 loop {
606 let oid = next.pop_front()?;
607 state.object_hash = oid.kind();
608 if state.hidden.contains_key(&oid) {
609 continue;
610 }
611 let mut parents: ParentIds = Default::default();
612 let generation;
613
614 match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) {
615 Ok(Either::CachedCommit(commit)) => {
616 generation = Some(commit.generation());
617 if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) {
618 self.cache = None;
620 return self.next_by_topology();
621 }
622
623 for (pid, _commit_time) in state.parent_ids.drain(..) {
624 parents.push(pid);
625 insert_into_seen_and_next(&mut state.seen, &state.hidden, pid, &mut self.predicate, next);
626 if matches!(self.parents, Parents::First) {
627 break;
628 }
629 }
630 }
631 Ok(Either::CommitRefIter(commit_iter)) => {
632 generation = None;
633 for token in commit_iter {
634 match token {
635 Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue,
636 Ok(gix_object::commit::ref_iter::Token::Parent { id: pid }) => {
637 parents.push(pid);
638 insert_into_seen_and_next(
639 &mut state.seen,
640 &state.hidden,
641 pid,
642 &mut self.predicate,
643 next,
644 );
645 if matches!(self.parents, Parents::First) {
646 break;
647 }
648 }
649 Ok(_a_token_past_the_parents) => break,
650 Err(err) => return Some(Err(err.into())),
651 }
652 }
653 }
654 Err(err) => return Some(Err(err.into())),
655 }
656
657 return Some(Ok(Info {
658 id: oid,
659 parent_ids: parents,
660 generation,
661 commit_time: None,
662 }));
663 }
664 }
665 }
666
667 fn insert_into_seen_and_next(
668 seen: &mut gix_hashtable::HashSet<ObjectId>,
669 hidden: &gix_revwalk::graph::IdMap<()>,
670 parent_id: ObjectId,
671 predicate: &mut impl FnMut(&oid) -> bool,
672 next: &mut VecDeque<ObjectId>,
673 ) {
674 if hidden.contains_key(&parent_id) {
675 return;
676 }
677 if seen.insert(parent_id) && predicate(&parent_id) {
678 next.push_back(parent_id);
679 }
680 }
681
682 #[expect(clippy::too_many_arguments)]
683 fn insert_into_seen_and_queue(
684 seen: &mut gix_hashtable::HashSet<ObjectId>,
685 hidden: &gix_revwalk::graph::IdMap<()>,
686 parent_id: ObjectId,
687 predicate: &mut impl FnMut(&oid) -> bool,
688 queue: &mut CommitDateQueue,
689 order: CommitTimeOrder,
690 cutoff: Option<SecondsSinceUnixEpoch>,
691 get_parent_commit_time: impl FnOnce() -> gix_date::SecondsSinceUnixEpoch,
692 ) {
693 if hidden.contains_key(&parent_id) {
694 return;
695 }
696 if seen.insert(parent_id) && predicate(&parent_id) {
697 let parent_commit_time = get_parent_commit_time();
698 let key = to_queue_key(parent_commit_time, order);
699 match cutoff {
700 Some(cutoff_older_than) if parent_commit_time < cutoff_older_than => {}
701 Some(_) | None => queue.insert(key, parent_id),
702 }
703 }
704 }
705}
706
707fn collect_parents(
708 dest: &mut SmallVec<[(gix_hash::ObjectId, gix_date::SecondsSinceUnixEpoch); 2]>,
709 cache: Option<&gix_commitgraph::Graph>,
710 parents: gix_commitgraph::file::commit::Parents<'_>,
711) -> bool {
712 dest.clear();
713 let cache = cache.as_ref().expect("parents iter is available, backed by `cache`");
714 for parent_id in parents {
715 match parent_id {
716 Ok(pos) => dest.push({
717 let parent = cache.commit_at(pos);
718 (
719 parent.id().to_owned(),
720 parent.committer_timestamp() as gix_date::SecondsSinceUnixEpoch,
721 )
722 }),
723 Err(_err) => return false,
724 }
725 }
726 true
727}