diff --git a/lib/src/index.rs b/lib/src/index.rs index 82d9f7ff4..a2e08d0fc 100644 --- a/lib/src/index.rs +++ b/lib/src/index.rs @@ -19,7 +19,7 @@ use std::fs::File; use std::hash::{Hash, Hasher}; use std::io; use std::io::{Cursor, Read, Write}; -use std::ops::Bound; +use std::ops::{Bound, Range}; use std::path::PathBuf; use std::sync::Arc; @@ -980,10 +980,20 @@ enum RevWalkWorkItemState { Unwanted, } -impl RevWalkWorkItem<'_, T> { +impl<'a, T> RevWalkWorkItem<'a, T> { fn is_wanted(&self) -> bool { matches!(self.state, RevWalkWorkItemState::Wanted(_)) } + + fn map_wanted(self, f: impl FnOnce(T) -> U) -> RevWalkWorkItem<'a, U> { + RevWalkWorkItem { + entry: self.entry, + state: match self.state { + RevWalkWorkItemState::Wanted(t) => RevWalkWorkItemState::Wanted(f(t)), + RevWalkWorkItemState::Unwanted => RevWalkWorkItemState::Unwanted, + }, + } + } } #[derive(Clone)] @@ -1002,6 +1012,18 @@ impl<'a, T: Ord> RevWalkQueue<'a, T> { } } + fn map_wanted(self, mut f: impl FnMut(T) -> U) -> RevWalkQueue<'a, U> { + RevWalkQueue { + index: self.index, + items: self + .items + .into_iter() + .map(|x| x.map_wanted(&mut f)) + .collect(), + unwanted_count: self.unwanted_count, + } + } + fn push_wanted(&mut self, pos: IndexPosition, t: T) { self.items.push(RevWalkWorkItem { entry: IndexEntryByPosition(self.index.entry_by_pos(pos)), @@ -1074,6 +1096,16 @@ impl<'a> RevWalk<'a> { fn add_unwanted(&mut self, pos: IndexPosition) { self.queue.push_unwanted(pos); } + + /// Filters entries by generation (or depth from the current wanted set.) + /// + /// The generation of the current wanted entries starts from 0. + pub fn filter_by_generation(self, generation_range: Range) -> RevWalkGenerationRange<'a> { + RevWalkGenerationRange { + queue: self.queue.map_wanted(|()| 0), + generation_range, + } + } } impl<'a> Iterator for RevWalk<'a> { @@ -1105,6 +1137,7 @@ impl<'a> Iterator for RevWalk<'a> { #[derive(Clone)] pub struct RevWalkGenerationRange<'a> { queue: RevWalkQueue<'a, u32>, + generation_range: Range, } impl<'a> Iterator for RevWalkGenerationRange<'a> { @@ -1113,7 +1146,10 @@ impl<'a> Iterator for RevWalkGenerationRange<'a> { fn next(&mut self) -> Option { while let Some(item) = self.queue.pop() { if let RevWalkWorkItemState::Wanted(mut known_gen) = item.state { - self.queue.push_wanted_parents(&item.entry.0, known_gen + 1); + let mut some_in_range = self.generation_range.contains(&known_gen); + if known_gen + 1 < self.generation_range.end { + self.queue.push_wanted_parents(&item.entry.0, known_gen + 1); + } while let Some(x) = self.queue.pop_eq(&item.entry.0) { // For wanted item, simply track all generation chains. This can // be optimized if the wanted range is just upper/lower bounded. @@ -1122,14 +1158,19 @@ impl<'a> Iterator for RevWalkGenerationRange<'a> { // merge overlapping generation ranges. match x.state { RevWalkWorkItemState::Wanted(gen) if known_gen != gen => { - self.queue.push_wanted_parents(&item.entry.0, gen + 1); + some_in_range |= self.generation_range.contains(&gen); + if gen + 1 < self.generation_range.end { + self.queue.push_wanted_parents(&item.entry.0, gen + 1); + } known_gen = gen; } RevWalkWorkItemState::Wanted(_) => {} RevWalkWorkItemState::Unwanted => unreachable!(), } } - return Some(item.entry.0); + if some_in_range { + return Some(item.entry.0); + } } else if self.queue.items.len() == self.queue.unwanted_count { // No more wanted entries to walk debug_assert!(!self.queue.items.iter().any(|x| x.is_wanted())); @@ -2130,6 +2171,87 @@ mod tests { ); } + #[test] + fn test_walk_revs_filter_by_generation() { + let mut index = MutableIndex::full(3); + // 8 6 + // | | + // 7 5 + // |/| + // 4 | + // | 3 + // 2 | + // |/ + // 1 + // | + // 0 + let id_0 = CommitId::from_hex("000000"); + let id_1 = CommitId::from_hex("111111"); + let id_2 = CommitId::from_hex("222222"); + let id_3 = CommitId::from_hex("333333"); + let id_4 = CommitId::from_hex("444444"); + let id_5 = CommitId::from_hex("555555"); + let id_6 = CommitId::from_hex("666666"); + let id_7 = CommitId::from_hex("777777"); + let id_8 = CommitId::from_hex("888888"); + index.add_commit_data(id_0.clone(), new_change_id(), &[]); + index.add_commit_data(id_1.clone(), new_change_id(), &[id_0.clone()]); + index.add_commit_data(id_2.clone(), new_change_id(), &[id_1.clone()]); + index.add_commit_data(id_3.clone(), new_change_id(), &[id_1.clone()]); + index.add_commit_data(id_4.clone(), new_change_id(), &[id_2.clone()]); + index.add_commit_data(id_5.clone(), new_change_id(), &[id_4.clone(), id_3.clone()]); + index.add_commit_data(id_6.clone(), new_change_id(), &[id_5.clone()]); + index.add_commit_data(id_7.clone(), new_change_id(), &[id_4.clone()]); + index.add_commit_data(id_8.clone(), new_change_id(), &[id_7.clone()]); + + let walk_commit_ids = |wanted: &[CommitId], unwanted: &[CommitId], range: Range| { + index + .walk_revs(wanted, unwanted) + .filter_by_generation(range) + .map(|entry| entry.commit_id()) + .collect_vec() + }; + + // Simple generation bounds + assert_eq!(walk_commit_ids(&[&id_8].map(Clone::clone), &[], 0..0), []); + assert_eq!( + walk_commit_ids(&[&id_2].map(Clone::clone), &[], 0..3), + [&id_2, &id_1, &id_0].map(Clone::clone) + ); + + // Ancestors may be walked with different generations + assert_eq!( + walk_commit_ids(&[&id_6].map(Clone::clone), &[], 2..4), + [&id_4, &id_3, &id_2, &id_1].map(Clone::clone) + ); + assert_eq!( + walk_commit_ids(&[&id_5].map(Clone::clone), &[], 2..3), + [&id_2, &id_1].map(Clone::clone) + ); + assert_eq!( + walk_commit_ids(&[&id_5, &id_7].map(Clone::clone), &[], 2..3), + [&id_2, &id_1].map(Clone::clone) + ); + assert_eq!( + walk_commit_ids(&[&id_7, &id_8].map(Clone::clone), &[], 0..2), + [&id_8, &id_7, &id_4].map(Clone::clone) + ); + assert_eq!( + walk_commit_ids(&[&id_6, &id_7].map(Clone::clone), &[], 0..3), + [&id_7, &id_6, &id_5, &id_4, &id_3, &id_2].map(Clone::clone) + ); + assert_eq!( + walk_commit_ids(&[&id_6, &id_7].map(Clone::clone), &[], 2..3), + [&id_4, &id_3, &id_2].map(Clone::clone) + ); + + // Ancestors of both wanted and unwanted commits are not walked + assert_eq!( + walk_commit_ids(&[&id_5].map(Clone::clone), &[&id_2].map(Clone::clone), 1..5), + [&id_4, &id_3].map(Clone::clone) + ); + } + #[test] fn test_heads() { let mut index = MutableIndex::full(3); diff --git a/lib/tests/test_index.rs b/lib/tests/test_index.rs index 0d476579c..c03415a7d 100644 --- a/lib/tests/test_index.rs +++ b/lib/tests/test_index.rs @@ -189,6 +189,7 @@ fn test_index_commits_criss_cross(use_git: bool) { } } + // RevWalk deduplicates chains by entry. assert_eq!( index .walk_revs(&[left_commits[num_generations - 1].id().clone()], &[]) @@ -219,6 +220,43 @@ fn test_index_commits_criss_cross(use_git: bool) { .count(), 2 ); + + // RevWalkGenerationRange deduplicates chains by (entry, generation), which may + // be more expensive than RevWalk, but should still finish in reasonable time. + assert_eq!( + index + .walk_revs(&[left_commits[num_generations - 1].id().clone()], &[]) + .filter_by_generation(0..(num_generations + 1) as u32) + .count(), + 2 * num_generations + ); + assert_eq!( + index + .walk_revs(&[right_commits[num_generations - 1].id().clone()], &[]) + .filter_by_generation(0..(num_generations + 1) as u32) + .count(), + 2 * num_generations + ); + assert_eq!( + index + .walk_revs( + &[left_commits[num_generations - 1].id().clone()], + &[left_commits[num_generations - 2].id().clone()] + ) + .filter_by_generation(0..(num_generations + 1) as u32) + .count(), + 2 + ); + assert_eq!( + index + .walk_revs( + &[right_commits[num_generations - 1].id().clone()], + &[right_commits[num_generations - 2].id().clone()] + ) + .filter_by_generation(0..(num_generations + 1) as u32) + .count(), + 2 + ); } #[test_case(false ; "local backend")]