diff --git a/Cargo.lock b/Cargo.lock index 23ad26be6b3..cae30bec939 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2678,6 +2678,7 @@ dependencies = [ name = "gix-traverse" version = "0.38.0" dependencies = [ + "bitflags 2.4.1", "gix-commitgraph 0.24.2", "gix-date 0.8.5", "gix-hash 0.14.2", diff --git a/examples/log.rs b/examples/log.rs index be892f37e15..c143f8544cc 100644 --- a/examples/log.rs +++ b/examples/log.rs @@ -8,7 +8,7 @@ use clap::Parser; use gix::{ bstr::{BString, ByteSlice}, date::time::format, - traverse::commit::Sorting, + traverse::commit::simple::Sorting, }; fn main() { diff --git a/gitoxide-core/src/hours/mod.rs b/gitoxide-core/src/hours/mod.rs index 0418ef1f01e..4080c30c4a0 100644 --- a/gitoxide-core/src/hours/mod.rs +++ b/gitoxide-core/src/hours/mod.rs @@ -175,7 +175,7 @@ where } commit_idx += 1; } - Err(gix::traverse::commit::ancestors::Error::Find { .. }) => { + Err(gix::traverse::commit::simple::Error::Find { .. }) => { is_shallow = true; break; } diff --git a/gitoxide-core/src/index/information.rs b/gitoxide-core/src/index/information.rs index aa618ab10c3..4540e5bc832 100644 --- a/gitoxide-core/src/index/information.rs +++ b/gitoxide-core/src/index/information.rs @@ -6,6 +6,7 @@ pub struct Options { #[cfg(feature = "serde")] mod serde_only { + use gix::index::entry::Stage; mod ext { #[derive(serde::Serialize)] @@ -115,11 +116,10 @@ mod serde_only { let (mut intent_to_add, mut skip_worktree) = (0, 0); for entry in f.entries() { match entry.flags.stage() { - 0 => stage_0_merged += 1, - 1 => stage_1_base += 1, - 2 => stage_2_ours += 1, - 3 => stage_3_theirs += 1, - invalid => anyhow::bail!("Invalid stage {} encountered", invalid), + Stage::Unconflicted => stage_0_merged += 1, + Stage::Base => stage_1_base += 1, + Stage::Ours => stage_2_ours += 1, + Stage::Theirs => stage_3_theirs += 1, } match entry.mode { gix::index::entry::Mode::DIR => dir += 1, diff --git a/gitoxide-core/src/pack/create.rs b/gitoxide-core/src/pack/create.rs index f8141e357d6..76bacd079c3 100644 --- a/gitoxide-core/src/pack/create.rs +++ b/gitoxide-core/src/pack/create.rs @@ -130,7 +130,7 @@ where .collect::, _>>()?; let handle = repo.objects.into_shared_arc().to_cache_arc(); let iter = Box::new( - traverse::commit::Ancestors::new(tips, traverse::commit::ancestors::State::default(), handle.clone()) + traverse::commit::Simple::new(tips, handle.clone()) .map(|res| res.map_err(|err| Box::new(err) as Box<_>).map(|c| c.id)) .inspect(move |_| progress.inc()), ); @@ -361,7 +361,7 @@ pub mod input_iteration { #[derive(Debug, thiserror::Error)] pub enum Error { #[error("input objects couldn't be iterated completely")] - Iteration(#[from] traverse::commit::ancestors::Error), + Iteration(#[from] traverse::commit::simple::Error), #[error("An error occurred while reading hashes from standard input")] InputLinesIo(#[from] std::io::Error), #[error("Could not decode hex hash provided on standard input")] diff --git a/gitoxide-core/src/query/engine/update.rs b/gitoxide-core/src/query/engine/update.rs index b5c6467e0d6..2e809f0e2b3 100644 --- a/gitoxide-core/src/query/engine/update.rs +++ b/gitoxide-core/src/query/engine/update.rs @@ -429,7 +429,7 @@ pub fn update( break; } } - Err(gix::traverse::commit::ancestors::Error::Find { .. }) => { + Err(gix::traverse::commit::simple::Error::Find { .. }) => { writeln!(err, "shallow repository - commit history is truncated").ok(); break; } diff --git a/gitoxide-core/src/repository/attributes/validate_baseline.rs b/gitoxide-core/src/repository/attributes/validate_baseline.rs index f2c92f71a30..a5571d45e86 100644 --- a/gitoxide-core/src/repository/attributes/validate_baseline.rs +++ b/gitoxide-core/src/repository/attributes/validate_baseline.rs @@ -262,6 +262,8 @@ pub(crate) mod function { } #[derive(Debug)] + // See note on `Mismatch` + #[allow(dead_code)] pub struct ExcludeLocation { pub line: usize, pub rela_source_file: String, @@ -269,6 +271,9 @@ pub(crate) mod function { } #[derive(Debug)] + // We debug-print this structure, which makes all fields 'used', but it doesn't count. + // TODO: find a way to not have to do more work, but make the warning go away. + #[allow(dead_code)] pub enum Mismatch { Attributes { actual: Vec, @@ -281,6 +286,8 @@ pub(crate) mod function { } #[derive(Debug)] + // See note on `Mismatch` + #[allow(dead_code)] pub struct ExcludeMatch { pub pattern: gix::glob::Pattern, pub source: Option, diff --git a/gitoxide-core/src/repository/commitgraph/list.rs b/gitoxide-core/src/repository/commitgraph/list.rs index 59e74a699cd..d9c024ed3f1 100644 --- a/gitoxide-core/src/repository/commitgraph/list.rs +++ b/gitoxide-core/src/repository/commitgraph/list.rs @@ -2,7 +2,7 @@ pub(crate) mod function { use std::{borrow::Cow, ffi::OsString}; use anyhow::{bail, Context}; - use gix::{prelude::ObjectIdExt, traverse::commit::Sorting}; + use gix::{prelude::ObjectIdExt, traverse::commit::simple::Sorting}; use crate::OutputFormat; diff --git a/gitoxide-core/src/repository/index/entries.rs b/gitoxide-core/src/repository/index/entries.rs index 7126849ad4d..3f17d55b3c3 100644 --- a/gitoxide-core/src/repository/index/entries.rs +++ b/gitoxide-core/src/repository/index/entries.rs @@ -23,6 +23,7 @@ pub(crate) mod function { io::{BufWriter, Write}, }; + use gix::index::entry::Stage; use gix::{ bstr::{BStr, BString}, worktree::IndexPersistedOrInMemory, @@ -392,11 +393,10 @@ pub(crate) mod function { out, "{} {}{:?} {} {}{}{}", match entry.flags.stage() { - 0 => " ", - 1 => "BASE ", - 2 => "OURS ", - 3 => "THEIRS ", - _ => "UNKNOWN", + Stage::Unconflicted => " ", + Stage::Base => "BASE ", + Stage::Ours => "OURS ", + Stage::Theirs => "THEIRS ", }, if entry.flags.is_empty() { "".to_string() diff --git a/gitoxide-core/src/repository/revision/list.rs b/gitoxide-core/src/repository/revision/list.rs index ab2bb2a60ed..7050bb6a667 100644 --- a/gitoxide-core/src/repository/revision/list.rs +++ b/gitoxide-core/src/repository/revision/list.rs @@ -17,7 +17,7 @@ pub const PROGRESS_RANGE: std::ops::RangeInclusive = 0..=2; pub(crate) mod function { use anyhow::{bail, Context}; - use gix::{hashtable::HashMap, traverse::commit::Sorting, Progress}; + use gix::{hashtable::HashMap, traverse::commit::simple::Sorting, Progress}; use layout::{ backends::svg::SVGWriter, core::{base::Orientation, geometry::Point, style::StyleAttr}, diff --git a/gix-config/src/file/impls.rs b/gix-config/src/file/impls.rs index 884e7ce346b..d6ac2b76a8f 100644 --- a/gix-config/src/file/impls.rs +++ b/gix-config/src/file/impls.rs @@ -42,7 +42,7 @@ impl<'a> TryFrom<&'a BStr> for File<'a> { impl From> for BString { fn from(c: File<'_>) -> Self { - c.into() + c.to_bstring() } } diff --git a/gix-config/src/file/includes/mod.rs b/gix-config/src/file/includes/mod.rs index 8fd92725fba..98a68482558 100644 --- a/gix-config/src/file/includes/mod.rs +++ b/gix-config/src/file/includes/mod.rs @@ -99,7 +99,14 @@ fn append_followed_includes_recursively( } buf.clear(); - std::io::copy(&mut std::fs::File::open(&config_path)?, buf)?; + std::io::copy( + &mut std::fs::File::open(&config_path).map_err(|err| Error::Io { + source: err, + path: config_path.to_owned(), + })?, + buf, + ) + .map_err(Error::CopyBuffer)?; let config_meta = Metadata { path: Some(config_path), trust: meta.trust, diff --git a/gix-config/src/file/includes/types.rs b/gix-config/src/file/includes/types.rs index 54b1eb5bfe0..85e3a35db89 100644 --- a/gix-config/src/file/includes/types.rs +++ b/gix-config/src/file/includes/types.rs @@ -1,11 +1,14 @@ use crate::{parse, path::interpolate}; +use std::path::PathBuf; /// The error returned when following includes. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { - #[error(transparent)] - Io(#[from] std::io::Error), + #[error("Failed to copy configuration file into buffer")] + CopyBuffer(#[source] std::io::Error), + #[error("Could not read included configuration file at '{}'", path.display())] + Io { path: PathBuf, source: std::io::Error }, #[error(transparent)] Parse(#[from] parse::Error), #[error(transparent)] diff --git a/gix-config/src/parse/comment.rs b/gix-config/src/parse/comment.rs index 6d4bb15ffb5..6d7ec145b65 100644 --- a/gix-config/src/parse/comment.rs +++ b/gix-config/src/parse/comment.rs @@ -39,7 +39,7 @@ impl Display for Comment<'_> { impl From> for BString { fn from(c: Comment<'_>) -> Self { - c.into() + c.to_bstring() } } diff --git a/gix-config/src/parse/event.rs b/gix-config/src/parse/event.rs index f528e2077d6..d88ace7ce6c 100644 --- a/gix-config/src/parse/event.rs +++ b/gix-config/src/parse/event.rs @@ -72,7 +72,7 @@ impl Display for Event<'_> { impl From> for BString { fn from(event: Event<'_>) -> Self { - event.into() + event.to_bstring() } } diff --git a/gix-config/src/parse/section/header.rs b/gix-config/src/parse/section/header.rs index ad1288f3fe0..5f4e382c90a 100644 --- a/gix-config/src/parse/section/header.rs +++ b/gix-config/src/parse/section/header.rs @@ -145,7 +145,7 @@ impl Display for Header<'_> { impl From> for BString { fn from(header: Header<'_>) -> Self { - header.into() + header.to_bstring() } } diff --git a/gix-diff/tests/tree/mod.rs b/gix-diff/tests/tree/mod.rs index 7df8f64c7c9..85fcbe6170c 100644 --- a/gix-diff/tests/tree/mod.rs +++ b/gix-diff/tests/tree/mod.rs @@ -129,11 +129,10 @@ mod changes { } fn all_commits(db: &gix_odb::Handle) -> HashMap { - use gix_traverse::commit; let mut buf = Vec::new(); let head = head_of(db); - commit::Ancestors::new(Some(head), commit::ancestors::State::default(), &db) + gix_traverse::commit::Simple::new(Some(head), &db) .collect::, _>>() .expect("valid iteration") .into_iter() diff --git a/gix-index/src/access/mod.rs b/gix-index/src/access/mod.rs index d5da3c9934a..dd2449c6d47 100644 --- a/gix-index/src/access/mod.rs +++ b/gix-index/src/access/mod.rs @@ -3,6 +3,7 @@ use std::{cmp::Ordering, ops::Range}; use bstr::{BStr, ByteSlice, ByteVec}; use filetime::FileTime; +use crate::entry::{Stage, StageRaw}; use crate::{entry, extension, AccelerateLookup, Entry, PathStorage, PathStorageRef, State, Version}; // TODO: integrate this somehow, somewhere, depending on later usage. @@ -81,7 +82,7 @@ impl State { res }) .ok()?; - self.entry_index_by_idx_and_stage(path, idx, stage, stage_cmp) + self.entry_index_by_idx_and_stage(path, idx, stage as StageRaw, stage_cmp) } /// Walk as far in `direction` as possible, with [`Ordering::Greater`] towards higher stages, and [`Ordering::Less`] @@ -112,7 +113,7 @@ impl State { &self, path: &BStr, idx: usize, - wanted_stage: entry::Stage, + wanted_stage: entry::StageRaw, stage_cmp: Ordering, ) -> Option { match stage_cmp { @@ -121,7 +122,7 @@ impl State { .enumerate() .rev() .take_while(|(_, e)| e.path(self) == path) - .find_map(|(idx, e)| (e.stage() == wanted_stage).then_some(idx)), + .find_map(|(idx, e)| (e.stage_raw() == wanted_stage).then_some(idx)), Ordering::Equal => Some(idx), Ordering::Less => self .entries @@ -129,7 +130,7 @@ impl State { .iter() .enumerate() .take_while(|(_, e)| e.path(self) == path) - .find_map(|(ofs, e)| (e.stage() == wanted_stage).then_some(idx + ofs + 1)), + .find_map(|(ofs, e)| (e.stage_raw() == wanted_stage).then_some(idx + ofs + 1)), } } @@ -291,7 +292,7 @@ impl State { .binary_search_by(|e| { let res = e.path(self).cmp(path); if res.is_eq() { - stage_at_index = e.stage(); + stage_at_index = e.stage_raw(); } res }) @@ -299,7 +300,7 @@ impl State { let idx = if stage_at_index == 0 || stage_at_index == 2 { idx } else { - self.entry_index_by_idx_and_stage(path, idx, 2, stage_at_index.cmp(&2))? + self.entry_index_by_idx_and_stage(path, idx, Stage::Ours as StageRaw, stage_at_index.cmp(&2))? }; Some(&self.entries[idx]) } @@ -334,13 +335,13 @@ impl State { + self.entries[low..].partition_point(|e| e.path(self).get(..prefix_len).map_or(false, |p| p <= prefix)); let low_entry = &self.entries.get(low)?; - if low_entry.stage() != 0 { + if low_entry.stage_raw() != 0 { low = self .walk_entry_stages(low_entry.path(self), low, Ordering::Less) .unwrap_or(low); } if let Some(high_entry) = self.entries.get(high) { - if high_entry.stage() != 0 { + if high_entry.stage_raw() != 0 { high = self .walk_entry_stages(high_entry.path(self), high, Ordering::Less) .unwrap_or(high); @@ -374,7 +375,7 @@ impl State { .binary_search_by(|e| { let res = e.path(self).cmp(path); if res.is_eq() { - stage_at_index = e.stage(); + stage_at_index = e.stage_raw(); } res }) diff --git a/gix-index/src/entry/flags.rs b/gix-index/src/entry/flags.rs index 05198bafb01..c32cb6f8d7a 100644 --- a/gix-index/src/entry/flags.rs +++ b/gix-index/src/entry/flags.rs @@ -62,6 +62,23 @@ bitflags! { impl Flags { /// Return the stage as extracted from the bits of this instance. pub fn stage(&self) -> Stage { + match self.stage_raw() { + 0 => Stage::Unconflicted, + 1 => Stage::Base, + 2 => Stage::Ours, + 3 => Stage::Theirs, + _ => unreachable!("BUG: Flags::STAGE_MASK is two bits, whose 4 possible values we have covered"), + } + } + + /// Return an entry's stage as raw number between 0 and 4. + /// Possible values are: + /// + /// * 0 = no conflict, + /// * 1 = base, + /// * 2 = ours, + /// * 3 = theirs + pub fn stage_raw(&self) -> u32 { (*self & Flags::STAGE_MASK).bits() >> 12 } diff --git a/gix-index/src/entry/mod.rs b/gix-index/src/entry/mod.rs index 75c8fa67121..5fd2e501930 100644 --- a/gix-index/src/entry/mod.rs +++ b/gix-index/src/entry/mod.rs @@ -1,9 +1,23 @@ -/// The stage of an entry, one of… +/// The stage of an entry. +#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub enum Stage { + /// This is the default, and most entries are in this stage. + #[default] + Unconflicted = 0, + /// The entry is the common base between 'our' change and 'their' change, for comparison. + Base = 1, + /// The entry represents our change. + Ours = 2, + /// The entry represents their change. + Theirs = 3, +} + +// The stage of an entry, one of… /// * 0 = no conflict, /// * 1 = base, /// * 2 = ours, /// * 3 = theirs -pub type Stage = u32; +pub type StageRaw = u32; /// #[allow(clippy::empty_docs)] @@ -78,6 +92,17 @@ mod access { pub fn stage(&self) -> entry::Stage { self.flags.stage() } + + /// Return an entry's stage as raw number between 0 and 4. + /// Possible values are: + /// + /// * 0 = no conflict, + /// * 1 = base, + /// * 2 = ours, + /// * 3 = theirs + pub fn stage_raw(&self) -> u32 { + self.flags.stage_raw() + } } } diff --git a/gix-index/src/extension/fs_monitor.rs b/gix-index/src/extension/fs_monitor.rs index 4bb5016ffe5..79d609dba19 100644 --- a/gix-index/src/extension/fs_monitor.rs +++ b/gix-index/src/extension/fs_monitor.rs @@ -6,6 +6,7 @@ use crate::{ }; #[derive(Clone)] +#[allow(dead_code)] pub enum Token { V1 { nanos_since_1970: u64 }, V2 { token: BString }, diff --git a/gix-index/src/lib.rs b/gix-index/src/lib.rs index 580b8e59718..a8ff94c0369 100644 --- a/gix-index/src/lib.rs +++ b/gix-index/src/lib.rs @@ -144,6 +144,7 @@ pub struct State { } mod impls { + use crate::entry::Stage; use std::fmt::{Debug, Formatter}; use crate::State; @@ -155,11 +156,10 @@ mod impls { f, "{} {}{:?} {} {}", match entry.flags.stage() { - 0 => " ", - 1 => "BASE ", - 2 => "OURS ", - 3 => "THEIRS ", - _ => "UNKNOWN", + Stage::Unconflicted => " ", + Stage::Base => "BASE ", + Stage::Ours => "OURS ", + Stage::Theirs => "THEIRS ", }, if entry.flags.is_empty() { "".to_string() diff --git a/gix-index/tests/index/access.rs b/gix-index/tests/index/access.rs index af76a45a295..d3e720486a4 100644 --- a/gix-index/tests/index/access.rs +++ b/gix-index/tests/index/access.rs @@ -1,5 +1,6 @@ use crate::index::Fixture; use bstr::{BString, ByteSlice}; +use gix_index::entry::Stage; fn icase_fixture() -> gix_index::File { Fixture::Generated("v2_icase_name_clashes").open() @@ -11,7 +12,7 @@ fn entry_by_path() { for entry in file.entries() { let path = entry.path(&file); assert_eq!(file.entry_by_path(path), Some(entry)); - assert_eq!(file.entry_by_path_and_stage(path, 0), Some(entry)); + assert_eq!(file.entry_by_path_and_stage(path, Stage::Unconflicted), Some(entry)); } } @@ -140,27 +141,27 @@ fn entry_by_path_and_stage() { for entry in file.entries() { let path = entry.path(&file); assert_eq!( - file.entry_index_by_path_and_stage(path, 0) + file.entry_index_by_path_and_stage(path, Stage::Unconflicted) .map(|idx| &file.entries()[idx]), Some(entry) ); - assert_eq!(file.entry_by_path_and_stage(path, 0), Some(entry)); + assert_eq!(file.entry_by_path_and_stage(path, Stage::Unconflicted), Some(entry)); } } #[test] fn entry_by_path_with_conflicting_file() { let file = Fixture::Loose("conflicting-file").open(); - for expected_stage in [1 /* common ancestor */, 2 /* ours */, 3 /* theirs */] { + for expected_stage in [Stage::Base, Stage::Ours, Stage::Theirs] { assert!( file.entry_by_path_and_stage("file".into(), expected_stage).is_some(), - "we have no stage 0 during a conflict, but all other ones. Missed {expected_stage}" + "we have no stage 0 during a conflict, but all other ones. Missed {expected_stage:?}" ); } assert_eq!( file.entry_by_path("file".into()).expect("found").stage(), - 2, + Stage::Ours, "we always find our stage while in a merge" ); } @@ -226,13 +227,13 @@ fn sort_entries() { for (idx, entry) in file.entries()[..valid_entries].iter().enumerate() { assert_eq!( - file.entry_index_by_path_and_stage_bounded(entry.path(&file), 0, valid_entries), + file.entry_index_by_path_and_stage_bounded(entry.path(&file), Stage::Unconflicted, valid_entries), Some(idx), "we can still find entries in the correctly sorted region" ); } assert_eq!( - file.entry_by_path_and_stage(new_entry_path, 0), + file.entry_by_path_and_stage(new_entry_path, Stage::Unconflicted), None, "new entry can't be found due to incorrect order" ); @@ -241,7 +242,7 @@ fn sort_entries() { assert!(file.verify_entries().is_ok(), "sorting of entries restores invariants"); assert_eq!( - file.entry_by_path_and_stage(new_entry_path, 0) + file.entry_by_path_and_stage(new_entry_path, Stage::Unconflicted) .expect("can be found") .path(&file), new_entry_path, diff --git a/gix-pack/src/cache/delta/from_offsets.rs b/gix-pack/src/cache/delta/from_offsets.rs index ee52f9ab9dd..fc807264d77 100644 --- a/gix-pack/src/cache/delta/from_offsets.rs +++ b/gix-pack/src/cache/delta/from_offsets.rs @@ -57,12 +57,14 @@ impl Tree { })?, ); - let anticipated_num_objects = if let Some(num_objects) = data_sorted_by_offsets.size_hint().1 { - progress.init(Some(num_objects), progress::count("objects")); - num_objects - } else { - 0 - }; + let anticipated_num_objects = data_sorted_by_offsets + .size_hint() + .1 + .map(|num_objects| { + progress.init(Some(num_objects), progress::count("objects")); + num_objects + }) + .unwrap_or_default(); let mut tree = Tree::with_capacity(anticipated_num_objects)?; { diff --git a/gix-pack/tests/pack/data/output/count_and_entries.rs b/gix-pack/tests/pack/data/output/count_and_entries.rs index a98f02d387e..c20bdf52727 100644 --- a/gix-pack/tests/pack/data/output/count_and_entries.rs +++ b/gix-pack/tests/pack/data/output/count_and_entries.rs @@ -9,7 +9,6 @@ use gix_pack::data::{ output, output::{count, entry}, }; -use gix_traverse::commit; use crate::pack::{ data::output::{db, DbKind}, @@ -241,7 +240,7 @@ fn traversals() -> crate::Result { .copied() { let head = hex_to_id("dfcb5e39ac6eb30179808bbab721e8a28ce1b52e"); - let mut commits = commit::Ancestors::new(Some(head), commit::ancestors::State::default(), db.clone()) + let mut commits = gix_traverse::commit::Simple::new(Some(head), db.clone()) .map(Result::unwrap) .map(|c| c.id) .collect::>(); diff --git a/gix-status/src/index_as_worktree/function.rs b/gix-status/src/index_as_worktree/function.rs index e69d6629b64..dbe7a838ed2 100644 --- a/gix-status/src/index_as_worktree/function.rs +++ b/gix-status/src/index_as_worktree/function.rs @@ -157,11 +157,11 @@ where let mut idx = 0; while let Some(entry) = chunk_entries.get(idx) { let absolute_entry_index = entry_offset + idx; - if idx == 0 && entry.stage() != 0 { + if idx == 0 && entry.stage_raw() != 0 { let offset = entry_offset.checked_sub(1).and_then(|prev_idx| { let prev_entry = &all_entries[prev_idx]; let entry_path = entry.path_in(state.path_backing); - if prev_entry.stage() == 0 || prev_entry.path_in(state.path_backing) != entry_path { + if prev_entry.stage_raw() == 0 || prev_entry.path_in(state.path_backing) != entry_path { // prev_entry (in previous chunk) does not belong to our conflict return None; } @@ -286,7 +286,7 @@ impl<'index> State<'_, 'index> { self.skipped_by_pathspec.fetch_add(1, Ordering::Relaxed); return None; } - let status = if entry.stage() != 0 { + let status = if entry.stage_raw() != 0 { Ok( Conflict::try_from_entry(entries, self.path_backing, entry_index, path).map(|(conflict, offset)| { *outer_entry_index += offset; // let out loop skip over entries related to the conflict @@ -604,7 +604,7 @@ impl Conflict { let mut count = 0_usize; for stage in (start_index..(start_index + 3).min(entries.len())).filter_map(|idx| { let entry = &entries[idx]; - let stage = entry.stage(); + let stage = entry.stage_raw(); (stage > 0 && entry.path_in(path_backing) == entry_path).then_some(stage) }) { // This could be `1 << (stage - 1)` but let's be specific. diff --git a/gix-traverse/Cargo.toml b/gix-traverse/Cargo.toml index 3024d8eeb15..cc505637b37 100644 --- a/gix-traverse/Cargo.toml +++ b/gix-traverse/Cargo.toml @@ -22,3 +22,4 @@ gix-revwalk = { version = "^0.13.0", path = "../gix-revwalk" } gix-commitgraph = { version = "^0.24.2", path = "../gix-commitgraph" } smallvec = "1.10.0" thiserror = "1.0.32" +bitflags = "2" diff --git a/gix-traverse/src/commit/mod.rs b/gix-traverse/src/commit/mod.rs new file mode 100644 index 00000000000..26e9287ae70 --- /dev/null +++ b/gix-traverse/src/commit/mod.rs @@ -0,0 +1,88 @@ +//! Provide multiple traversal implementations with different performance envelopes. +//! +//! Use [`Simple`] for fast walks that maintain minimal state, or [`Topo`] for a more elaborate traversal. +use gix_hash::ObjectId; +use gix_object::FindExt; +use gix_revwalk::graph::IdMap; +use gix_revwalk::PriorityQueue; +use smallvec::SmallVec; + +/// A fast iterator over the ancestors of one or more starting commits. +pub struct Simple { + objects: Find, + cache: Option, + predicate: Predicate, + state: simple::State, + parents: Parents, + sorting: simple::Sorting, +} + +/// Simple ancestors traversal, without the need to keep track of graph-state. +pub mod simple; + +/// A commit walker that walks in topographical order, like `git rev-list +/// --topo-order` or `--date-order` depending on the chosen [`topo::Sorting`]. +/// +/// Instantiate with [`topo::Builder`]. +pub struct Topo { + commit_graph: Option, + find: Find, + predicate: Predicate, + indegrees: IdMap, + states: IdMap, + explore_queue: PriorityQueue, + indegree_queue: PriorityQueue, + topo_queue: topo::iter::Queue, + parents: Parents, + min_gen: u32, + buf: Vec, +} + +pub mod topo; + +/// Specify how to handle commit parents during traversal. +#[derive(Default, Copy, Clone)] +pub enum Parents { + /// Traverse all parents, useful for traversing the entire ancestry. + #[default] + All, + /// Only traverse along the first parent, which commonly ignores all branches. + First, +} + +/// The collection of parent ids we saw as part of the iteration. +/// +/// Note that this list is truncated if [`Parents::First`] was used. +pub type ParentIds = SmallVec<[gix_hash::ObjectId; 1]>; + +/// Information about a commit that we obtained naturally as part of the iteration. +#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub struct Info { + /// The id of the commit. + pub id: gix_hash::ObjectId, + /// All parent ids we have encountered. Note that these will be at most one if [`Parents::First`] is enabled. + pub parent_ids: ParentIds, + /// The time at which the commit was created. It will only be `Some(_)` if the chosen traversal was + /// taking dates into consideration. + pub commit_time: Option, +} + +enum Either<'buf, 'cache> { + CommitRefIter(gix_object::CommitRefIter<'buf>), + CachedCommit(gix_commitgraph::file::Commit<'cache>), +} + +fn find<'cache, 'buf, Find>( + cache: Option<&'cache gix_commitgraph::Graph>, + objects: Find, + id: &gix_hash::oid, + buf: &'buf mut Vec, +) -> Result, gix_object::find::existing_iter::Error> +where + Find: gix_object::Find, +{ + match cache.and_then(|cache| cache.commit_by_id(id).map(Either::CachedCommit)) { + Some(c) => Ok(c), + None => objects.find_commit_iter(id, buf).map(Either::CommitRefIter), + } +} diff --git a/gix-traverse/src/commit.rs b/gix-traverse/src/commit/simple.rs similarity index 74% rename from gix-traverse/src/commit.rs rename to gix-traverse/src/commit/simple.rs index c1d18ebf6ff..a4a3ff391c8 100644 --- a/gix-traverse/src/commit.rs +++ b/gix-traverse/src/commit/simple.rs @@ -1,27 +1,10 @@ -use gix_object::FindExt; +use gix_date::SecondsSinceUnixEpoch; +use gix_hash::ObjectId; +use gix_hashtable::HashSet; use smallvec::SmallVec; +use std::collections::VecDeque; -/// An iterator over the ancestors one or more starting commits -pub struct Ancestors { - objects: Find, - cache: Option, - predicate: Predicate, - state: StateMut, - parents: Parents, - sorting: Sorting, -} - -/// Specify how to handle commit parents during traversal. -#[derive(Default, Copy, Clone)] -pub enum Parents { - /// Traverse all parents, useful for traversing the entire ancestry. - #[default] - All, - /// Only traverse along the first parent, which commonly ignores all branches. - First, -} - -/// Specify how to sort commits during traversal. +/// Specify how to sort commits during a [simple](super::Simple) traversal. /// /// ### Sample History /// @@ -33,7 +16,6 @@ pub enum Parents { /// \ \ /// 3----5----6----8--- /// ``` - #[derive(Default, Debug, Copy, Clone)] pub enum Sorting { /// Commits are sorted as they are mentioned in the commit graph. @@ -69,59 +51,38 @@ pub enum Sorting { }, } -/// The collection of parent ids we saw as part of the iteration. -/// -/// Note that this list is truncated if [`Parents::First`] was used. -pub type ParentIds = SmallVec<[gix_hash::ObjectId; 1]>; +/// The error is part of the item returned by the [Ancestors](super::Simple) iterator. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error(transparent)] + Find(#[from] gix_object::find::existing_iter::Error), + #[error(transparent)] + ObjectDecode(#[from] gix_object::decode::Error), +} -/// Information about a commit that we obtained naturally as part of the iteration. -#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] -pub struct Info { - /// The id of the commit. - pub id: gix_hash::ObjectId, - /// All parent ids we have encountered. Note that these will be at most one if [`Parents::First`] is enabled. - pub parent_ids: ParentIds, - /// The time at which the commit was created. It's only `Some(_)` if sorting is not [`Sorting::BreadthFirst`], as the walk - /// needs to require the commit-date. - pub commit_time: Option, +/// The state used and potentially shared by multiple graph traversals. +#[derive(Clone)] +pub(super) struct State { + next: VecDeque, + queue: gix_revwalk::PriorityQueue, + buf: Vec, + seen: HashSet, + parents_buf: Vec, + parent_ids: SmallVec<[(ObjectId, SecondsSinceUnixEpoch); 2]>, } /// #[allow(clippy::empty_docs)] -pub mod ancestors { - use std::{ - borrow::{Borrow, BorrowMut}, - collections::VecDeque, - }; - +mod init { use gix_date::SecondsSinceUnixEpoch; use gix_hash::{oid, ObjectId}; - use gix_hashtable::HashSet; use gix_object::{CommitRefIter, FindExt}; - use smallvec::SmallVec; - - use crate::commit::{collect_parents, Ancestors, Either, Info, ParentIds, Parents, Sorting}; - /// The error is part of the item returned by the [Ancestors] iterator. - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error(transparent)] - Find(#[from] gix_object::find::existing_iter::Error), - #[error(transparent)] - ObjectDecode(#[from] gix_object::decode::Error), - } - - /// The state used and potentially shared by multiple graph traversals. - #[derive(Clone)] - pub struct State { - next: VecDeque, - queue: gix_revwalk::PriorityQueue, - buf: Vec, - seen: HashSet, - parents_buf: Vec, - parent_ids: SmallVec<[(ObjectId, SecondsSinceUnixEpoch); 2]>, - } + use super::{ + super::{simple::Sorting, Either, Info, ParentIds, Parents, Simple}, + collect_parents, Error, State, + }; impl Default for State { fn default() -> Self { @@ -146,12 +107,11 @@ pub mod ancestors { } /// Builder - impl Ancestors + impl Simple where Find: gix_object::Find, - StateMut: BorrowMut, { - /// Set the sorting method, either topological or by author date + /// Set the `sorting` method. pub fn sorting(mut self, sorting: Sorting) -> Result { self.sorting = sorting; match self.sorting { @@ -160,7 +120,7 @@ pub mod ancestors { } Sorting::ByCommitTimeNewestFirst | Sorting::ByCommitTimeNewestFirstCutoffOlderThan { .. } => { let cutoff_time = self.sorting.cutoff_time(); - let state = self.state.borrow_mut(); + let state = &mut self.state; for commit_id in state.next.drain(..) { let commit_iter = self.objects.find_commit_iter(&commit_id, &mut state.buf)?; let time = commit_iter.committer()?.time.seconds; @@ -198,7 +158,7 @@ pub mod ancestors { } fn queue_to_vecdeque(&mut self) { - let state = self.state.borrow_mut(); + let state = &mut self.state; state.next.extend( std::mem::replace(&mut state.queue, gix_revwalk::PriorityQueue::new()) .into_iter_unordered() @@ -207,41 +167,35 @@ pub mod ancestors { } } - /// Initialization - impl Ancestors bool, StateMut> + /// Lifecyle + impl Simple bool> where Find: gix_object::Find, - StateMut: BorrowMut, { /// Create a new instance. /// /// * `find` - a way to lookup new object data during traversal by their `ObjectId`, writing their data into buffer and returning /// an iterator over commit tokens if the object is present and is a commit. Caching should be implemented within this function /// as needed. - /// * `state` - all state used for the traversal. If multiple traversals are performed, allocations can be minimized by reusing - /// this state. /// * `tips` /// * the starting points of the iteration, usually commits /// * each commit they lead to will only be returned once, including the tip that started it - pub fn new(tips: impl IntoIterator>, state: StateMut, find: Find) -> Self { - Self::filtered(tips, state, find, |_| true) + pub fn new(tips: impl IntoIterator>, find: Find) -> Self { + Self::filtered(tips, find, |_| true) } } - /// Initialization - impl Ancestors + /// Lifecyle + impl Simple where Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, - StateMut: BorrowMut, { /// Create a new instance with commit filtering enabled. /// /// * `find` - a way to lookup new object data during traversal by their `ObjectId`, writing their data into buffer and returning /// an iterator over commit tokens if the object is present and is a commit. Caching should be implemented within this function /// as needed. - /// * `state` - all state used for the traversal. If multiple traversals are performed, allocations can be minimized by reusing - /// this state. /// * `tips` /// * the starting points of the iteration, usually commits /// * each commit they lead to will only be returned once, including the tip that started it @@ -249,13 +203,12 @@ pub mod ancestors { /// as whether its parent commits should be traversed. pub fn filtered( tips: impl IntoIterator>, - mut state: StateMut, find: Find, mut predicate: Predicate, ) -> Self { let tips = tips.into_iter(); + let mut state = State::default(); { - let state = state.borrow_mut(); state.clear(); state.next.reserve(tips.size_hint().0); for tip in tips.map(Into::into) { @@ -275,27 +228,24 @@ pub mod ancestors { } } } + /// Access - impl Ancestors - where - StateMut: Borrow, - { - /// Return an iterator for accessing more of the current commits data. + impl Simple { + /// Return an iterator for accessing data of the current commit, parsed lazily. pub fn commit_iter(&self) -> CommitRefIter<'_> { - CommitRefIter::from_bytes(&self.state.borrow().buf) + CommitRefIter::from_bytes(&self.state.buf) } - /// Return the current commits data. + /// Return the current commits' raw data, which can be parsed using [`gix_object::CommitRef::from_bytes()`]. pub fn commit_data(&self) -> &[u8] { - &self.state.borrow().buf + &self.state.buf } } - impl Iterator for Ancestors + impl Iterator for Simple where Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, - StateMut: BorrowMut, { type Item = Result; @@ -325,21 +275,20 @@ pub mod ancestors { } /// Utilities - impl Ancestors + impl Simple where Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, - StateMut: BorrowMut, { fn next_by_commit_date( &mut self, cutoff_older_than: Option, ) -> Option> { - let state = self.state.borrow_mut(); + let state = &mut self.state; let (commit_time, oid) = state.queue.pop()?; let mut parents: ParentIds = Default::default(); - match super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) { + match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) { Ok(Either::CachedCommit(commit)) => { if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) { // drop corrupt caches and try again with ODB @@ -396,17 +345,16 @@ pub mod ancestors { } /// Utilities - impl Ancestors + impl Simple where Find: gix_object::Find, Predicate: FnMut(&oid) -> bool, - StateMut: BorrowMut, { fn next_by_topology(&mut self) -> Option> { - let state = self.state.borrow_mut(); + let state = &mut self.state; let oid = state.next.pop_front()?; let mut parents: ParentIds = Default::default(); - match super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) { + match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) { Ok(Either::CachedCommit(commit)) => { if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) { // drop corrupt caches and try again with ODB @@ -455,11 +403,6 @@ pub mod ancestors { } } -enum Either<'buf, 'cache> { - CommitRefIter(gix_object::CommitRefIter<'buf>), - CachedCommit(gix_commitgraph::file::Commit<'cache>), -} - fn collect_parents( dest: &mut SmallVec<[(gix_hash::ObjectId, gix_date::SecondsSinceUnixEpoch); 2]>, cache: Option<&gix_commitgraph::Graph>, @@ -481,18 +424,3 @@ fn collect_parents( } true } - -fn find<'cache, 'buf, Find>( - cache: Option<&'cache gix_commitgraph::Graph>, - objects: Find, - id: &gix_hash::oid, - buf: &'buf mut Vec, -) -> Result, gix_object::find::existing_iter::Error> -where - Find: gix_object::Find, -{ - match cache.and_then(|cache| cache.commit_by_id(id).map(Either::CachedCommit)) { - Some(c) => Ok(c), - None => objects.find_commit_iter(id, buf).map(Either::CommitRefIter), - } -} diff --git a/gix-traverse/src/commit/topo/init.rs b/gix-traverse/src/commit/topo/init.rs new file mode 100644 index 00000000000..42972d2b871 --- /dev/null +++ b/gix-traverse/src/commit/topo/init.rs @@ -0,0 +1,174 @@ +use crate::commit::topo::iter::gen_and_commit_time; +use crate::commit::topo::{Error, Sorting, WalkFlags}; +use crate::commit::{find, Info, Parents, Topo}; +use gix_hash::{oid, ObjectId}; +use gix_revwalk::graph::IdMap; +use gix_revwalk::PriorityQueue; + +/// Builder for [`Topo`]. +pub struct Builder { + commit_graph: Option, + find: Find, + predicate: Predicate, + sorting: Sorting, + parents: Parents, + tips: Vec, + ends: Vec, +} + +impl Builder bool> +where + Find: gix_object::Find, +{ + /// Create a new `Builder` for a [`Topo`] that reads commits from a repository with `find`. + /// starting at the `tips` and ending at the `ends`. Like `git rev-list + /// --topo-order ^ends... tips...`. + pub fn from_iters( + find: Find, + tips: impl IntoIterator>, + ends: Option>>, + ) -> Self { + let tips = tips.into_iter().map(Into::into).collect::>(); + let ends = ends + .map(|e| e.into_iter().map(Into::into).collect::>()) + .unwrap_or_default(); + + Self { + commit_graph: Default::default(), + find, + sorting: Default::default(), + parents: Default::default(), + tips, + ends, + predicate: |_| true, + } + } + + /// Set a `predicate` to filter out revisions from the walk. Can be used to + /// implement e.g. filtering on paths or time. This does *not* exclude the + /// parent(s) of a revision that is excluded. Specify a revision as an 'end' + /// if you want that behavior. + pub fn with_predicate(self, predicate: Predicate) -> Builder + where + Predicate: FnMut(&oid) -> bool, + { + Builder { + commit_graph: self.commit_graph, + find: self.find, + sorting: self.sorting, + parents: self.parents, + tips: self.tips, + ends: self.ends, + predicate, + } + } +} + +impl Builder +where + Find: gix_object::Find, + Predicate: FnMut(&oid) -> bool, +{ + /// Set the `sorting` to use for the topological walk. + pub fn sorting(mut self, sorting: Sorting) -> Self { + self.sorting = sorting; + self + } + + /// Specify how to handle commit `parents` during traversal. + pub fn parents(mut self, parents: Parents) -> Self { + self.parents = parents; + self + } + + /// Set or unset the `commit_graph` to use for the iteration. + pub fn with_commit_graph(mut self, commit_graph: Option) -> Self { + self.commit_graph = commit_graph; + self + } + + /// Build a new [`Topo`] instance. + /// + /// Note that merely building an instance is currently expensive. + pub fn build(self) -> Result, Error> { + let mut w = Topo { + commit_graph: self.commit_graph, + find: self.find, + predicate: self.predicate, + indegrees: IdMap::default(), + states: IdMap::default(), + explore_queue: PriorityQueue::new(), + indegree_queue: PriorityQueue::new(), + topo_queue: super::iter::Queue::new(self.sorting), + parents: self.parents, + min_gen: gix_commitgraph::GENERATION_NUMBER_INFINITY, + buf: vec![], + }; + + // Initial flags for the states of the tips and ends. All of them are + // seen and added to the explore and indegree queues. The ends are by + // definition (?) uninteresting and bottom. + let tip_flags = WalkFlags::Seen | WalkFlags::Explored | WalkFlags::InDegree; + let end_flags = tip_flags | WalkFlags::Uninteresting | WalkFlags::Bottom; + + for (id, flags) in self + .tips + .iter() + .map(|id| (id, tip_flags)) + .chain(self.ends.iter().map(|id| (id, end_flags))) + { + *w.indegrees.entry(*id).or_default() = 1; + let commit = find(w.commit_graph.as_ref(), &w.find, id, &mut w.buf)?; + let (gen, time) = gen_and_commit_time(commit)?; + + if gen < w.min_gen { + w.min_gen = gen; + } + + w.states.insert(*id, flags); + w.explore_queue.insert((gen, time), *id); + w.indegree_queue.insert((gen, time), *id); + } + + // NOTE: Parents of the ends must also be marked uninteresting for some + // reason. See handle_commit() + for id in &self.ends { + let parents = w.collect_all_parents(id)?; + for (id, _) in parents { + w.states + .entry(id) + .and_modify(|s| *s |= WalkFlags::Uninteresting) + .or_insert(WalkFlags::Uninteresting | WalkFlags::Seen); + } + } + + w.compute_indegrees_to_depth(w.min_gen)?; + + // NOTE: in Git the ends are also added to the topo_queue in addition to + // the tips, but then in simplify_commit() Git is told to ignore it. For + // now the tests pass. + for id in self.tips.iter() { + let i = w.indegrees.get(id).ok_or(Error::MissingIndegreeUnexpected)?; + + if *i != 1 { + continue; + } + + let commit = find(w.commit_graph.as_ref(), &w.find, id, &mut w.buf)?; + let (_, time) = gen_and_commit_time(commit)?; + let parent_ids = w.collect_all_parents(id)?.into_iter().map(|e| e.0).collect(); + + w.topo_queue.push( + time, + Info { + id: *id, + parent_ids, + commit_time: Some(time), + }, + ); + } + + w.topo_queue.initial_sort(); + Ok(w) + } +} diff --git a/gix-traverse/src/commit/topo/iter.rs b/gix-traverse/src/commit/topo/iter.rs new file mode 100644 index 00000000000..09f38eb7e7a --- /dev/null +++ b/gix-traverse/src/commit/topo/iter.rs @@ -0,0 +1,312 @@ +use crate::commit::topo::{Error, Sorting, WalkFlags}; +use crate::commit::{find, Either, Info, Parents, Topo}; +use gix_hash::{oid, ObjectId}; +use gix_revwalk::PriorityQueue; +use smallvec::SmallVec; + +pub(in crate::commit) type GenAndCommitTime = (u32, i64); + +// Git's priority queue works as a LIFO stack if no compare function is set, +// which is the case for `--topo-order.` However, even in that case the initial +// items of the queue are sorted according to the commit time before beginning +// the walk. +#[derive(Debug)] +pub(in crate::commit) enum Queue { + Date(PriorityQueue), + Topo(Vec<(i64, Info)>), +} + +impl Queue { + pub(super) fn new(s: Sorting) -> Self { + match s { + Sorting::DateOrder => Self::Date(PriorityQueue::new()), + Sorting::TopoOrder => Self::Topo(vec![]), + } + } + + pub(super) fn push(&mut self, commit_time: i64, info: Info) { + match self { + Self::Date(q) => q.insert(commit_time, info), + Self::Topo(q) => q.push((commit_time, info)), + } + } + + fn pop(&mut self) -> Option { + match self { + Self::Date(q) => q.pop().map(|(_, info)| info), + Self::Topo(q) => q.pop().map(|(_, info)| info), + } + } + + pub(super) fn initial_sort(&mut self) { + if let Self::Topo(ref mut inner_vec) = self { + inner_vec.sort_by(|a, b| a.0.cmp(&b.0)); + } + } +} + +impl Topo +where + Find: gix_object::Find, +{ + pub(super) fn compute_indegrees_to_depth(&mut self, gen_cutoff: u32) -> Result<(), Error> { + while let Some(((gen, _), _)) = self.indegree_queue.peek() { + if *gen >= gen_cutoff { + self.indegree_walk_step()?; + } else { + break; + } + } + + Ok(()) + } + + fn indegree_walk_step(&mut self) -> Result<(), Error> { + if let Some(((gen, _), id)) = self.indegree_queue.pop() { + self.explore_to_depth(gen)?; + + let parents = self.collect_parents(&id)?; + for (id, gen_time) in parents { + self.indegrees.entry(id).and_modify(|e| *e += 1).or_insert(2); + + let state = self.states.get_mut(&id).ok_or(Error::MissingStateUnexpected)?; + if !state.contains(WalkFlags::InDegree) { + *state |= WalkFlags::InDegree; + self.indegree_queue.insert(gen_time, id); + } + } + } + Ok(()) + } + + fn explore_to_depth(&mut self, gen_cutoff: u32) -> Result<(), Error> { + while let Some(((gen, _), _)) = self.explore_queue.peek() { + if *gen >= gen_cutoff { + self.explore_walk_step()?; + } else { + break; + } + } + Ok(()) + } + + fn explore_walk_step(&mut self) -> Result<(), Error> { + if let Some((_, id)) = self.explore_queue.pop() { + let parents = self.collect_parents(&id)?; + self.process_parents(&id, &parents)?; + + for (id, gen_time) in parents { + let state = self.states.get_mut(&id).ok_or(Error::MissingStateUnexpected)?; + + if !state.contains(WalkFlags::Explored) { + *state |= WalkFlags::Explored; + self.explore_queue.insert(gen_time, id); + } + } + } + Ok(()) + } + + fn expand_topo_walk(&mut self, id: &oid) -> Result<(), Error> { + let parents = self.collect_parents(id)?; + self.process_parents(id, &parents)?; + + for (pid, (parent_gen, parent_commit_time)) in parents { + let parent_state = self.states.get(&pid).ok_or(Error::MissingStateUnexpected)?; + if parent_state.contains(WalkFlags::Uninteresting) { + continue; + } + + if parent_gen < self.min_gen { + self.min_gen = parent_gen; + self.compute_indegrees_to_depth(self.min_gen)?; + } + + let i = self.indegrees.get_mut(&pid).ok_or(Error::MissingIndegreeUnexpected)?; + *i -= 1; + if *i != 1 { + continue; + } + + let parent_ids = self.collect_all_parents(&pid)?.into_iter().map(|e| e.0).collect(); + self.topo_queue.push( + parent_commit_time, + Info { + id: pid, + parent_ids, + commit_time: Some(parent_commit_time), + }, + ); + } + + Ok(()) + } + + fn process_parents(&mut self, id: &oid, parents: &[(ObjectId, GenAndCommitTime)]) -> Result<(), Error> { + let state = self.states.get_mut(id).ok_or(Error::MissingStateUnexpected)?; + if state.contains(WalkFlags::Added) { + return Ok(()); + } + + *state |= WalkFlags::Added; + + // If the current commit is uninteresting we pass that on to ALL + // parents, otherwise we set the Seen flag. + let (pass, insert) = if state.contains(WalkFlags::Uninteresting) { + let flags = WalkFlags::Uninteresting; + for (id, _) in parents { + let grand_parents = self.collect_all_parents(id)?; + + for (id, _) in &grand_parents { + self.states + .entry(*id) + .and_modify(|s| *s |= WalkFlags::Uninteresting) + .or_insert(WalkFlags::Uninteresting | WalkFlags::Seen); + } + } + (flags, flags) + } else { + // NOTE: git sets SEEN like we do but keeps the SYMMETRIC_LEFT and + // ANCENSTRY_PATH if they are set, but they have no purpose here. + let flags = WalkFlags::empty(); + (flags, WalkFlags::Seen) + }; + + for (id, _) in parents { + self.states.entry(*id).and_modify(|s| *s |= pass).or_insert(insert); + } + Ok(()) + } + + fn collect_parents(&mut self, id: &oid) -> Result, Error> { + collect_parents( + self.commit_graph.as_ref(), + &self.find, + id, + matches!(self.parents, Parents::First), + &mut self.buf, + ) + } + + // Same as collect_parents but disregards the first_parent flag + pub(super) fn collect_all_parents( + &mut self, + id: &oid, + ) -> Result, Error> { + collect_parents(self.commit_graph.as_ref(), &self.find, id, false, &mut self.buf) + } + + fn pop_commit(&mut self) -> Option> { + let commit = self.topo_queue.pop()?; + let i = match self.indegrees.get_mut(&commit.id) { + Some(i) => i, + None => { + return Some(Err(Error::MissingIndegreeUnexpected)); + } + }; + + *i = 0; + if let Err(e) = self.expand_topo_walk(&commit.id) { + return Some(Err(e)); + }; + + Some(Ok(commit)) + } +} + +impl Iterator for Topo +where + Find: gix_object::Find, + Predicate: FnMut(&oid) -> bool, +{ + type Item = Result; + + fn next(&mut self) -> Option { + loop { + match self.pop_commit()? { + Ok(id) => { + if (self.predicate)(&id.id) { + return Some(Ok(id)); + } + } + Err(e) => return Some(Err(e)), + } + } + } +} + +fn collect_parents( + cache: Option<&gix_commitgraph::Graph>, + f: Find, + id: &oid, + first_only: bool, + buf: &mut Vec, +) -> Result, Error> +where + Find: gix_object::Find, +{ + let mut parents = SmallVec::<[(ObjectId, GenAndCommitTime); 1]>::new(); + match find(cache, &f, id, buf)? { + Either::CommitRefIter(c) => { + for token in c { + use gix_object::commit::ref_iter::Token as T; + match token { + Ok(T::Tree { .. }) => continue, + Ok(T::Parent { id }) => { + parents.push((id, (0, 0))); // Dummy numbers to be filled in + if first_only { + break; + } + } + Ok(_past_parents) => break, + Err(err) => return Err(err.into()), + } + } + // Need to check the cache again. That a commit is not in the cache + // doesn't mean a parent is not. + for (id, gen_time) in parents.iter_mut() { + let commit = find(cache, &f, id, buf)?; + *gen_time = gen_and_commit_time(commit)?; + } + } + Either::CachedCommit(c) => { + for pos in c.iter_parents() { + let parent_commit = cache + .expect("cache exists if CachedCommit was returned") + .commit_at(pos?); + parents.push(( + parent_commit.id().into(), + (parent_commit.generation(), parent_commit.committer_timestamp() as i64), + )); + if first_only { + break; + } + } + } + }; + Ok(parents) +} + +pub(super) fn gen_and_commit_time(c: Either<'_, '_>) -> Result { + match c { + Either::CommitRefIter(c) => { + let mut commit_time = 0; + for token in c { + use gix_object::commit::ref_iter::Token as T; + match token { + Ok(T::Tree { .. }) => continue, + Ok(T::Parent { .. }) => continue, + Ok(T::Author { .. }) => continue, + Ok(T::Committer { signature }) => { + commit_time = signature.time.seconds; + break; + } + Ok(_unused_token) => break, + Err(err) => return Err(err.into()), + } + } + Ok((gix_commitgraph::GENERATION_NUMBER_INFINITY, commit_time)) + } + Either::CachedCommit(c) => Ok((c.generation(), c.committer_timestamp() as i64)), + } +} diff --git a/gix-traverse/src/commit/topo/mod.rs b/gix-traverse/src/commit/topo/mod.rs new file mode 100644 index 00000000000..6ae543c106c --- /dev/null +++ b/gix-traverse/src/commit/topo/mod.rs @@ -0,0 +1,70 @@ +//! Topological commit traversal, similar to `git log --topo-order`, which keeps track of graph state. + +use bitflags::bitflags; + +/// The errors that can occur during creation and iteration. +#[derive(thiserror::Error, Debug)] +#[allow(missing_docs)] +pub enum Error { + #[error("Indegree information is missing")] + MissingIndegreeUnexpected, + #[error("Internal state (bitflags) not found")] + MissingStateUnexpected, + #[error(transparent)] + CommitGraphFile(#[from] gix_commitgraph::file::commit::Error), + #[error(transparent)] + ObjectDecode(#[from] gix_object::decode::Error), + #[error(transparent)] + Find(#[from] gix_object::find::existing_iter::Error), +} + +bitflags! { + /// Set of flags to describe the state of a particular commit while iterating. + // NOTE: The names correspond to the names of the flags in revision.h + #[repr(transparent)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub(super) struct WalkFlags: u32 { + /// Commit has been seen + const Seen = 0b000001; + /// Commit has been processed by the Explore walk + const Explored = 0b000010; + /// Commit has been processed by the Indegree walk + const InDegree = 0b000100; + /// Commit is deemed uninteresting for whatever reason + const Uninteresting = 0b001000; + /// Commit marks the end of a walk, like `foo` in `git rev-list foo..bar` + const Bottom = 0b010000; + /// Parents have been processed + const Added = 0b100000; + } +} + +/// Sorting to use for the topological walk. +/// +/// ### Sample History +/// +/// The following history will be referred to for explaining how the sort order works, with the number denoting the commit timestamp +/// (*their X-alignment doesn't matter*). +/// +/// ```text +/// ---1----2----4----7 <- second parent of 8 +/// \ \ +/// 3----5----6----8--- +/// ``` +#[derive(Clone, Copy, Debug, Default)] +pub enum Sorting { + /// Show no parents before all of its children are shown, but otherwise show + /// commits in the commit timestamp order. + #[default] + DateOrder, + /// Show no parents before all of its children are shown, and avoid + /// showing commits on multiple lines of history intermixed. + /// + /// In the *sample history* the order would be `8, 6, 5, 3, 7, 4, 2, 1` + TopoOrder, +} + +mod init; +pub use init::Builder; + +pub(super) mod iter; diff --git a/gix-traverse/src/lib.rs b/gix-traverse/src/lib.rs index 3cf6d2b3af7..49776318332 100644 --- a/gix-traverse/src/lib.rs +++ b/gix-traverse/src/lib.rs @@ -2,7 +2,6 @@ #![deny(missing_docs, rust_2018_idioms)] #![forbid(unsafe_code)] -/// Commit traversal pub mod commit; /// Tree traversal diff --git a/gix-traverse/tests/commit/mod.rs b/gix-traverse/tests/commit/mod.rs index 6d9fac3e9c8..7aa327bfa53 100644 --- a/gix-traverse/tests/commit/mod.rs +++ b/gix-traverse/tests/commit/mod.rs @@ -1,430 +1,2 @@ -mod ancestor { - use gix_hash::{oid, ObjectId}; - use gix_traverse::commit; - - use crate::hex_to_id; - - struct TraversalAssertion<'a> { - init_script: &'a str, - repo_name: &'a str, - tips: &'a [&'a str], - expected: &'a [&'a str], - mode: commit::Parents, - sorting: commit::Sorting, - } - - impl<'a> TraversalAssertion<'a> { - fn new(init_script: &'a str, tips: &'a [&'a str], expected: &'a [&'a str]) -> Self { - Self::new_at(init_script, "", tips, expected) - } - - fn new_at(init_script: &'a str, repo_name: &'a str, tips: &'a [&'a str], expected: &'a [&'a str]) -> Self { - TraversalAssertion { - init_script, - repo_name, - tips, - expected, - mode: Default::default(), - sorting: Default::default(), - } - } - - fn with_parents(&mut self, mode: commit::Parents) -> &mut Self { - self.mode = mode; - self - } - - fn with_sorting(&mut self, sorting: commit::Sorting) -> &mut Self { - self.sorting = sorting; - self - } - } - - impl TraversalAssertion<'_> { - fn setup(&self) -> crate::Result<(gix_odb::Handle, Vec, Vec)> { - let dir = gix_testtools::scripted_fixture_read_only_standalone(self.init_script)?; - let store = gix_odb::at(dir.join(self.repo_name).join(".git").join("objects"))?; - let tips: Vec<_> = self.tips.iter().copied().map(hex_to_id).collect(); - let expected: Vec = tips - .clone() - .into_iter() - .chain(self.expected.iter().map(|hex_id| hex_to_id(hex_id))) - .collect(); - Ok((store, tips, expected)) - } - - fn setup_commitgraph(&self, store: &gix_odb::Store, use_graph: bool) -> Option { - use_graph - .then(|| gix_commitgraph::at(store.path().join("info"))) - .transpose() - .expect("graph can be loaded if it exists") - } - - fn check_with_predicate(&mut self, predicate: impl FnMut(&oid) -> bool + Clone) -> crate::Result<()> { - let (store, tips, expected) = self.setup()?; - - for use_commitgraph in [false, true] { - let oids = commit::Ancestors::filtered( - tips.clone(), - commit::ancestors::State::default(), - &store, - predicate.clone(), - ) - .sorting(self.sorting)? - .parents(self.mode) - .commit_graph(self.setup_commitgraph(store.store_ref(), use_commitgraph)) - .map(|res| res.map(|info| info.id)) - .collect::, _>>()?; - - assert_eq!(oids, expected); - } - Ok(()) - } - - fn check(&self) -> crate::Result { - let (store, tips, expected) = self.setup()?; - - for use_commitgraph in [false, true] { - let oids = commit::Ancestors::new(tips.clone(), commit::ancestors::State::default(), &store) - .sorting(self.sorting)? - .parents(self.mode) - .commit_graph(self.setup_commitgraph(store.store_ref(), use_commitgraph)) - .map(|res| res.map(|info| info.id)) - .collect::, _>>()?; - assert_eq!(oids, expected); - } - Ok(()) - } - } - - mod different_date_intermixed { - use gix_traverse::commit::Sorting; - - use crate::commit::ancestor::TraversalAssertion; - - #[test] - fn head_breadth_first() -> crate::Result { - TraversalAssertion::new_at( - "make_repos.sh", - "intermixed", - &["58912d92944087dcb09dca79cdd2a937cc158bed"], /* merge */ - // This is very different from what git does as it keeps commits together, - // whereas we spread them out breadth-first. - &[ - "2dce37be587e07caef8c4a5ab60b423b13a8536a", /* c3 */ - "0f6632a5a7d81417488b86692b729e49c1b73056", /* b1c2 */ - "a9c28710e058af4e5163699960234adb9fb2abc7", /* b2c2 */ - "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ - "77fd3c6832c0cd542f7a39f3af9250c3268db979", /* b1c1 */ - "b648f955b930ca95352fae6f22cb593ee0244b27", /* b2c1 */ - "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ - ], - ) - .check() - } - - #[test] - fn head_date_order() -> crate::Result { - TraversalAssertion::new_at( - "make_repos.sh", - "intermixed", - &["58912d92944087dcb09dca79cdd2a937cc158bed"], /* merge */ - // This is exactly what git shows. - &[ - "2dce37be587e07caef8c4a5ab60b423b13a8536a", /* c3 */ - "0f6632a5a7d81417488b86692b729e49c1b73056", /* b1c2 */ - "a9c28710e058af4e5163699960234adb9fb2abc7", /* b2c2 */ - "77fd3c6832c0cd542f7a39f3af9250c3268db979", /* b1c1 */ - "b648f955b930ca95352fae6f22cb593ee0244b27", /* b2c1 */ - "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ - "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ - ], - ) - .with_sorting(Sorting::ByCommitTimeNewestFirst) - .check() - } - } - - mod different_date { - use gix_traverse::commit::Sorting; - - use crate::commit::ancestor::TraversalAssertion; - - #[test] - fn head_breadth_first() -> crate::Result { - TraversalAssertion::new_at( - "make_repos.sh", - "simple", - &["f49838d84281c3988eeadd988d97dd358c9f9dc4"], /* merge */ - // This is very different from what git does as it keeps commits together, - // whereas we spread them out breadth-first. - &[ - "0edb95c0c0d9933d88f532ec08fcd405d0eee882", /* c5 */ - "66a309480201c4157b0eae86da69f2d606aadbe7", /* b1c2 */ - "48e8dac19508f4238f06c8de2b10301ce64a641c", /* b2c2 */ - "8cb5f13b66ce52a49399a2c49f537ee2b812369c", /* c4 */ - "80947acb398362d8236fcb8bf0f8a9dac640583f", /* b1c1 */ - "cb6a6befc0a852ac74d74e0354e0f004af29cb79", /* b2c1 */ - "33aa07785dd667c0196064e3be3c51dd9b4744ef", /* c3 */ - "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ - "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ - ], - ) - .check() - } - - #[test] - fn head_date_order() -> crate::Result { - TraversalAssertion::new_at( - "make_repos.sh", - "simple", - &["f49838d84281c3988eeadd988d97dd358c9f9dc4"], /* merge */ - // This is exactly what git shows. - &[ - "0edb95c0c0d9933d88f532ec08fcd405d0eee882", /* c5 */ - "66a309480201c4157b0eae86da69f2d606aadbe7", /* b1c2 */ - "80947acb398362d8236fcb8bf0f8a9dac640583f", /* b1c1 */ - "48e8dac19508f4238f06c8de2b10301ce64a641c", /* b2c2 */ - "cb6a6befc0a852ac74d74e0354e0f004af29cb79", /* b2c1 */ - "8cb5f13b66ce52a49399a2c49f537ee2b812369c", /* c4 */ - "33aa07785dd667c0196064e3be3c51dd9b4744ef", /* c3 */ - "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ - "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ - ], - ) - .with_sorting(Sorting::ByCommitTimeNewestFirst) - .check() - } - } - - /// Same dates are somewhat special as they show how sorting-details on priority queues affects ordering - mod same_date { - use gix_traverse::commit::{Parents, Sorting}; - - use crate::{commit::ancestor::TraversalAssertion, hex_to_id}; - - #[test] - fn c4_breadth_first() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_same_date.sh", - &["9556057aee5abb06912922e9f26c46386a816822"], /* c4 */ - &[ - "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .check() - } - - #[test] - fn head_breadth_first() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_same_date.sh", - &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ - // We always take the first parent first, then the second, and so on. - // Deviation: git for some reason displays b1c2 *before* c5, but I think it's better - // to have a strict parent order. - &[ - "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ - "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ - "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ - "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ - "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .check() - } - - #[test] - fn head_date_order() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_same_date.sh", - &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ - &[ - "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ - "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ - "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ - "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ - "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .with_sorting(Sorting::ByCommitTimeNewestFirst) - .check() - } - - #[test] - fn head_first_parent_only_breadth_first() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_same_date.sh", - &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ - &[ - "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ - "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ - "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .with_parents(Parents::First) - .check() - } - - #[test] - fn head_c4_breadth_first() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_same_date.sh", - &[ - "01ec18a3ebf2855708ad3c9d244306bc1fae3e9b", /* m1b1 */ - "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ - ], - &[ - "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ - "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ - "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ - "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .check() - } - - #[test] - fn filtered_commit_does_not_block_ancestors_reachable_from_another_commit() -> crate::Result { - // I don't see a use case for the predicate returning false for a commit but return true for - // at least one of its ancestors, so this test is kind of dubious. But we do want - // `Ancestors` to not eagerly blacklist all of a commit's ancestors when blacklisting that - // one commit, and this test happens to check that. - TraversalAssertion::new( - "make_traversal_repo_for_commits_same_date.sh", - &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ - &[ - "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ - "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ - "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ - "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .check_with_predicate(|id| id != hex_to_id("9152eeee2328073cf23dcf8e90c949170b711659")) - } - - #[test] - fn predicate_only_called_once_even_if_fork_point() -> crate::Result { - // The `self.seen` check should come before the `self.predicate` check, as we don't know how - // expensive calling `self.predicate` may be. - let mut seen = false; - TraversalAssertion::new( - "make_traversal_repo_for_commits_same_date.sh", - &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ - &[ - "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ - "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ - "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ - ], - ) - .check_with_predicate(move |id| { - if id == hex_to_id("9556057aee5abb06912922e9f26c46386a816822") { - assert!(!seen); - seen = true; - false - } else { - true - } - }) - } - } - - /// Some dates adjusted to be a year apart, but still 'c1' and 'c2' with the same date. - mod adjusted_dates { - use gix_traverse::commit::{ancestors, Ancestors, Parents, Sorting}; - - use crate::{commit::ancestor::TraversalAssertion, hex_to_id}; - - #[test] - fn head_breadth_first() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_with_dates.sh", - &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ - // Here `git` also shows `b1c1` first, making topo-order similar to date order for some reason, - // even though c2 *is* the first parent. - &[ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac", /* b1c1 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .check() - } - - #[test] - fn head_date_order() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_with_dates.sh", - &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ - &[ - "bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac", /* b1c1 */ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .with_sorting(Sorting::ByCommitTimeNewestFirst) - .check() - } - - #[test] - fn head_date_order_with_cutoff() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_with_dates.sh", - &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ - &["bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac"], /* b1c1 */ - ) - .with_sorting(Sorting::ByCommitTimeNewestFirstCutoffOlderThan { - seconds: 978393600, // =2001-01-02 00:00:00 +0000 - }) - .check() - } - - #[test] - fn date_order_with_cutoff_is_applied_to_starting_position() -> crate::Result { - let dir = - gix_testtools::scripted_fixture_read_only_standalone("make_traversal_repo_for_commits_with_dates.sh")?; - let store = gix_odb::at(dir.join(".git").join("objects"))?; - let iter = Ancestors::new( - Some(hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7" /* c2 */)), - ancestors::State::default(), - &store, - ) - .sorting(Sorting::ByCommitTimeNewestFirstCutoffOlderThan { - seconds: 978393600, // =2001-01-02 00:00:00 +0000 - })?; - assert_eq!( - iter.count(), - 0, - "initial tips that don't pass cutoff value are not returned either" - ); - Ok(()) - } - - #[test] - fn head_date_order_first_parent_only() -> crate::Result { - TraversalAssertion::new( - "make_traversal_repo_for_commits_with_dates.sh", - &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ - &[ - "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ - "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ - ], - ) - .with_sorting(Sorting::ByCommitTimeNewestFirst) - .with_parents(Parents::First) - .check() - } - } -} +mod simple; +mod topo; diff --git a/gix-traverse/tests/commit/simple.rs b/gix-traverse/tests/commit/simple.rs new file mode 100644 index 00000000000..983941f35de --- /dev/null +++ b/gix-traverse/tests/commit/simple.rs @@ -0,0 +1,422 @@ +use gix_hash::{oid, ObjectId}; +use gix_traverse::commit; + +use crate::hex_to_id; + +struct TraversalAssertion<'a> { + init_script: &'a str, + repo_name: &'a str, + tips: &'a [&'a str], + expected: &'a [&'a str], + mode: commit::Parents, + sorting: commit::simple::Sorting, +} + +impl<'a> TraversalAssertion<'a> { + fn new(init_script: &'a str, tips: &'a [&'a str], expected: &'a [&'a str]) -> Self { + Self::new_at(init_script, "", tips, expected) + } + + fn new_at(init_script: &'a str, repo_name: &'a str, tips: &'a [&'a str], expected: &'a [&'a str]) -> Self { + TraversalAssertion { + init_script, + repo_name, + tips, + expected, + mode: Default::default(), + sorting: Default::default(), + } + } + + fn with_parents(&mut self, mode: commit::Parents) -> &mut Self { + self.mode = mode; + self + } + + fn with_sorting(&mut self, sorting: commit::simple::Sorting) -> &mut Self { + self.sorting = sorting; + self + } +} + +impl TraversalAssertion<'_> { + fn setup(&self) -> crate::Result<(gix_odb::Handle, Vec, Vec)> { + let dir = gix_testtools::scripted_fixture_read_only_standalone(self.init_script)?; + let store = gix_odb::at(dir.join(self.repo_name).join(".git").join("objects"))?; + let tips: Vec<_> = self.tips.iter().copied().map(hex_to_id).collect(); + let expected: Vec = tips + .clone() + .into_iter() + .chain(self.expected.iter().map(|hex_id| hex_to_id(hex_id))) + .collect(); + Ok((store, tips, expected)) + } + + fn setup_commitgraph(&self, store: &gix_odb::Store, use_graph: bool) -> Option { + use_graph + .then(|| gix_commitgraph::at(store.path().join("info"))) + .transpose() + .expect("graph can be loaded if it exists") + } + + fn check_with_predicate(&mut self, predicate: impl FnMut(&oid) -> bool + Clone) -> crate::Result<()> { + let (store, tips, expected) = self.setup()?; + + for use_commitgraph in [false, true] { + let oids = commit::Simple::filtered(tips.clone(), &store, predicate.clone()) + .sorting(self.sorting)? + .parents(self.mode) + .commit_graph(self.setup_commitgraph(store.store_ref(), use_commitgraph)) + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + assert_eq!(oids, expected); + } + Ok(()) + } + + fn check(&self) -> crate::Result { + let (store, tips, expected) = self.setup()?; + + for use_commitgraph in [false, true] { + let oids = commit::Simple::new(tips.clone(), &store) + .sorting(self.sorting)? + .parents(self.mode) + .commit_graph(self.setup_commitgraph(store.store_ref(), use_commitgraph)) + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + assert_eq!(oids, expected); + } + Ok(()) + } +} + +mod different_date_intermixed { + use gix_traverse::commit::simple::Sorting; + + use crate::commit::simple::TraversalAssertion; + + #[test] + fn head_breadth_first() -> crate::Result { + TraversalAssertion::new_at( + "make_repos.sh", + "intermixed", + &["58912d92944087dcb09dca79cdd2a937cc158bed"], /* merge */ + // This is very different from what git does as it keeps commits together, + // whereas we spread them out breadth-first. + &[ + "2dce37be587e07caef8c4a5ab60b423b13a8536a", /* c3 */ + "0f6632a5a7d81417488b86692b729e49c1b73056", /* b1c2 */ + "a9c28710e058af4e5163699960234adb9fb2abc7", /* b2c2 */ + "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ + "77fd3c6832c0cd542f7a39f3af9250c3268db979", /* b1c1 */ + "b648f955b930ca95352fae6f22cb593ee0244b27", /* b2c1 */ + "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ + ], + ) + .check() + } + + #[test] + fn head_date_order() -> crate::Result { + TraversalAssertion::new_at( + "make_repos.sh", + "intermixed", + &["58912d92944087dcb09dca79cdd2a937cc158bed"], /* merge */ + // This is exactly what git shows. + &[ + "2dce37be587e07caef8c4a5ab60b423b13a8536a", /* c3 */ + "0f6632a5a7d81417488b86692b729e49c1b73056", /* b1c2 */ + "a9c28710e058af4e5163699960234adb9fb2abc7", /* b2c2 */ + "77fd3c6832c0cd542f7a39f3af9250c3268db979", /* b1c1 */ + "b648f955b930ca95352fae6f22cb593ee0244b27", /* b2c1 */ + "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ + "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ + ], + ) + .with_sorting(Sorting::ByCommitTimeNewestFirst) + .check() + } +} + +mod different_date { + use gix_traverse::commit::simple::Sorting; + + use crate::commit::simple::TraversalAssertion; + + #[test] + fn head_breadth_first() -> crate::Result { + TraversalAssertion::new_at( + "make_repos.sh", + "simple", + &["f49838d84281c3988eeadd988d97dd358c9f9dc4"], /* merge */ + // This is very different from what git does as it keeps commits together, + // whereas we spread them out breadth-first. + &[ + "0edb95c0c0d9933d88f532ec08fcd405d0eee882", /* c5 */ + "66a309480201c4157b0eae86da69f2d606aadbe7", /* b1c2 */ + "48e8dac19508f4238f06c8de2b10301ce64a641c", /* b2c2 */ + "8cb5f13b66ce52a49399a2c49f537ee2b812369c", /* c4 */ + "80947acb398362d8236fcb8bf0f8a9dac640583f", /* b1c1 */ + "cb6a6befc0a852ac74d74e0354e0f004af29cb79", /* b2c1 */ + "33aa07785dd667c0196064e3be3c51dd9b4744ef", /* c3 */ + "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ + "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ + ], + ) + .check() + } + + #[test] + fn head_date_order() -> crate::Result { + TraversalAssertion::new_at( + "make_repos.sh", + "simple", + &["f49838d84281c3988eeadd988d97dd358c9f9dc4"], /* merge */ + // This is exactly what git shows. + &[ + "0edb95c0c0d9933d88f532ec08fcd405d0eee882", /* c5 */ + "66a309480201c4157b0eae86da69f2d606aadbe7", /* b1c2 */ + "80947acb398362d8236fcb8bf0f8a9dac640583f", /* b1c1 */ + "48e8dac19508f4238f06c8de2b10301ce64a641c", /* b2c2 */ + "cb6a6befc0a852ac74d74e0354e0f004af29cb79", /* b2c1 */ + "8cb5f13b66ce52a49399a2c49f537ee2b812369c", /* c4 */ + "33aa07785dd667c0196064e3be3c51dd9b4744ef", /* c3 */ + "ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83", /* c2 */ + "65d6af66f60b8e39fd1ba6a1423178831e764ec5", /* c1 */ + ], + ) + .with_sorting(Sorting::ByCommitTimeNewestFirst) + .check() + } +} + +/// Same dates are somewhat special as they show how sorting-details on priority queues affects ordering +mod same_date { + use gix_traverse::commit::{simple::Sorting, Parents}; + + use crate::{commit::simple::TraversalAssertion, hex_to_id}; + + #[test] + fn c4_breadth_first() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_same_date.sh", + &["9556057aee5abb06912922e9f26c46386a816822"], /* c4 */ + &[ + "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .check() + } + + #[test] + fn head_breadth_first() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_same_date.sh", + &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ + // We always take the first parent first, then the second, and so on. + // Deviation: git for some reason displays b1c2 *before* c5, but I think it's better + // to have a strict parent order. + &[ + "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ + "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ + "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ + "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ + "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .check() + } + + #[test] + fn head_date_order() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_same_date.sh", + &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ + &[ + "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ + "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ + "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ + "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ + "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .with_sorting(Sorting::ByCommitTimeNewestFirst) + .check() + } + + #[test] + fn head_first_parent_only_breadth_first() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_same_date.sh", + &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ + &[ + "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ + "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ + "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .with_parents(Parents::First) + .check() + } + + #[test] + fn head_c4_breadth_first() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_same_date.sh", + &[ + "01ec18a3ebf2855708ad3c9d244306bc1fae3e9b", /* m1b1 */ + "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ + ], + &[ + "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ + "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ + "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ + "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .check() + } + + #[test] + fn filtered_commit_does_not_block_ancestors_reachable_from_another_commit() -> crate::Result { + // I don't see a use case for the predicate returning false for a commit but return true for + // at least one of its ancestors, so this test is kind of dubious. But we do want + // `Ancestors` to not eagerly blacklist all of a commit's ancestors when blacklisting that + // one commit, and this test happens to check that. + TraversalAssertion::new( + "make_traversal_repo_for_commits_same_date.sh", + &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ + &[ + "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ + "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ + "9556057aee5abb06912922e9f26c46386a816822", /* c4 */ + "17d78c64cef6c33a10a604573fd2c429e477fd63", /* c3 */ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .check_with_predicate(|id| id != hex_to_id("9152eeee2328073cf23dcf8e90c949170b711659")) + } + + #[test] + fn predicate_only_called_once_even_if_fork_point() -> crate::Result { + // The `self.seen` check should come before the `self.predicate` check, as we don't know how + // expensive calling `self.predicate` may be. + let mut seen = false; + TraversalAssertion::new( + "make_traversal_repo_for_commits_same_date.sh", + &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"], /* m1b1 */ + &[ + "efd9a841189668f1bab5b8ebade9cd0a1b139a37", /* c5 */ + "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353", /* b1c2 */ + "9152eeee2328073cf23dcf8e90c949170b711659", /* b1c1 */ + ], + ) + .check_with_predicate(move |id| { + if id == hex_to_id("9556057aee5abb06912922e9f26c46386a816822") { + assert!(!seen); + seen = true; + false + } else { + true + } + }) + } +} + +/// Some dates adjusted to be a year apart, but still 'c1' and 'c2' with the same date. +mod adjusted_dates { + use gix_traverse::commit::{simple::Sorting, Parents, Simple}; + + use crate::{commit::simple::TraversalAssertion, hex_to_id}; + + #[test] + fn head_breadth_first() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_with_dates.sh", + &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ + // Here `git` also shows `b1c1` first, making topo-order similar to date order for some reason, + // even though c2 *is* the first parent. + &[ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac", /* b1c1 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .check() + } + + #[test] + fn head_date_order() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_with_dates.sh", + &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ + &[ + "bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac", /* b1c1 */ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .with_sorting(Sorting::ByCommitTimeNewestFirst) + .check() + } + + #[test] + fn head_date_order_with_cutoff() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_with_dates.sh", + &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ + &["bcb05040a6925f2ff5e10d3ae1f9264f2e8c43ac"], /* b1c1 */ + ) + .with_sorting(Sorting::ByCommitTimeNewestFirstCutoffOlderThan { + seconds: 978393600, // =2001-01-02 00:00:00 +0000 + }) + .check() + } + + #[test] + fn date_order_with_cutoff_is_applied_to_starting_position() -> crate::Result { + let dir = + gix_testtools::scripted_fixture_read_only_standalone("make_traversal_repo_for_commits_with_dates.sh")?; + let store = gix_odb::at(dir.join(".git").join("objects"))?; + let iter = Simple::new( + Some(hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7" /* c2 */)), + &store, + ) + .sorting(Sorting::ByCommitTimeNewestFirstCutoffOlderThan { + seconds: 978393600, // =2001-01-02 00:00:00 +0000 + })?; + assert_eq!( + iter.count(), + 0, + "initial tips that don't pass cutoff value are not returned either" + ); + Ok(()) + } + + #[test] + fn head_date_order_first_parent_only() -> crate::Result { + TraversalAssertion::new( + "make_traversal_repo_for_commits_with_dates.sh", + &["288e509293165cb5630d08f4185bdf2445bf6170"], /* m1b1 */ + &[ + "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7", /* c2 */ + "134385f6d781b7e97062102c6a483440bfda2a03", /* c1 */ + ], + ) + .with_sorting(Sorting::ByCommitTimeNewestFirst) + .with_parents(Parents::First) + .check() + } +} diff --git a/gix-traverse/tests/commit/topo.rs b/gix-traverse/tests/commit/topo.rs new file mode 100644 index 00000000000..ff28a0b0c95 --- /dev/null +++ b/gix-traverse/tests/commit/topo.rs @@ -0,0 +1,372 @@ +use gix_hash::{oid, ObjectId}; +use gix_object::bstr::ByteSlice; +use gix_traverse::commit::{topo, Parents}; +use std::path::PathBuf; + +use crate::hex_to_id; + +struct TraversalAssertion<'a> { + init_script: &'a str, + worktree_dir: PathBuf, + repo_name: &'a str, + tips: &'a [&'a str], + ends: &'a [&'a str], + expected: &'a [&'a str], + mode: Parents, + sorting: topo::Sorting, +} + +/// API +impl<'a> TraversalAssertion<'a> { + fn new(tips: &'a [&'a str], ends: &'a [&'a str], expected: &'a [&'a str]) -> Self { + Self::new_at("make_repo_for_topo.sh", "", tips, ends, expected) + } + + fn new_at( + init_script: &'a str, + repo_name: &'a str, + tips: &'a [&'a str], + ends: &'a [&'a str], + expected: &'a [&'a str], + ) -> Self { + TraversalAssertion { + init_script, + worktree_dir: Default::default(), + repo_name, + tips, + ends, + expected, + mode: Default::default(), + sorting: Default::default(), + } + } + + fn with_parents(&mut self, mode: Parents) -> &mut Self { + self.mode = mode; + self + } + + fn with_sorting(&mut self, sorting: topo::Sorting) -> &mut Self { + self.sorting = sorting; + self + } + + fn check_with_predicate(&mut self, predicate: impl FnMut(&oid) -> bool + Clone) -> crate::Result<()> { + let (store, tips, ends, expected) = self.setup()?; + + for use_commitgraph in [false, true] { + let oids = topo::Builder::from_iters(&store, tips.iter().copied(), Some(ends.iter().copied())) + .sorting(self.sorting) + .with_commit_graph(self.setup_commitgraph(store.store_ref(), use_commitgraph)) + .parents(self.mode) + .with_predicate(predicate.clone()) + .build()? + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + assert_eq!(oids, expected); + } + Ok(()) + } + + fn assert_baseline(&self, name: &str) { + let buf = std::fs::read(self.worktree_dir.join(format!("{name}.baseline"))) + .expect("a baseline must be set for each repo"); + let expected: Vec<_> = buf.lines().map(|s| s.to_str().unwrap()).collect(); + assert_eq!( + self.expected, expected, + "Baseline must match the expectation we provide here" + ); + } + + fn check(&mut self) -> crate::Result { + let (store, tips, ends, expected) = self.setup()?; + + for use_commitgraph in [false, true] { + let oids = topo::Builder::from_iters(&store, tips.iter().copied(), Some(ends.iter().copied())) + .sorting(self.sorting) + .with_commit_graph(self.setup_commitgraph(store.store_ref(), use_commitgraph)) + .parents(self.mode) + .build()? + .map(|res| res.map(|info| info.id)) + .collect::, _>>()?; + + assert_eq!(oids, expected); + } + Ok(()) + } +} + +impl TraversalAssertion<'_> { + #[allow(clippy::type_complexity)] + fn setup(&mut self) -> crate::Result<(gix_odb::Handle, Vec, Vec, Vec)> { + let dir = gix_testtools::scripted_fixture_read_only_standalone(self.init_script)?; + let worktree_dir = dir.join(self.repo_name); + let store = gix_odb::at(worktree_dir.join(".git").join("objects"))?; + self.worktree_dir = worktree_dir; + + let tips: Vec<_> = self.tips.iter().copied().map(hex_to_id).collect(); + let ends: Vec<_> = self.ends.iter().copied().map(hex_to_id).collect(); + // `tips` is not chained with expected unlike in `commit`'s + // TraversalAssertion since it's not given that all the tips are + // shown first. + let expected: Vec = self.expected.iter().map(|hex_id| hex_to_id(hex_id)).collect(); + + Ok((store, tips, ends, expected)) + } + + fn setup_commitgraph(&self, store: &gix_odb::Store, use_graph: bool) -> Option { + use_graph + .then(|| gix_commitgraph::at(store.path().join("info"))) + .transpose() + .expect("graph can be loaded if it exists") + } +} + +mod basic { + use gix_traverse::commit::topo; + + use super::TraversalAssertion; + + use crate::hex_to_id; + + #[test] + fn simple() -> crate::Result { + let mut assertion = TraversalAssertion::new( + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &[], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + "f1cce1b5c7efcdfa106e95caa6c45a2cae48a481", + "945d8a360915631ad545e0cf04630d86d3d4eaa1", + "a863c02247a6c5ba32dff5224459f52aa7f77f7b", + "2f291881edfb0597493a52d26ea09dd7340ce507", + "9c46b8765703273feb10a2ebd810e70b8e2ca44a", + "fb3e21cf45b04b617011d2b30973f3e5ce60d0cd", + ], + ); + assertion.with_sorting(topo::Sorting::TopoOrder).check()?; + assertion.assert_baseline("all-commits"); + Ok(()) + } + + #[test] + fn one_end() -> crate::Result { + TraversalAssertion::new( + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &["f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + ], + ) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } + + #[test] + fn empty_range() -> crate::Result { + TraversalAssertion::new( + &["f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"], + &["eeab3243aad67bc838fc4425f759453bf0b47785"], + &[], + ) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } + + #[test] + fn two_tips_two_ends() -> crate::Result { + TraversalAssertion::new( + &[ + "d09384f312b03e4a1413160739805ff25e8fe99d", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + ], + &[ + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + ], + &[ + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + ], + ) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } + + #[test] + fn with_dummy_predicate() -> crate::Result { + TraversalAssertion::new( + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &[], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + "f1cce1b5c7efcdfa106e95caa6c45a2cae48a481", + "945d8a360915631ad545e0cf04630d86d3d4eaa1", + "a863c02247a6c5ba32dff5224459f52aa7f77f7b", + "2f291881edfb0597493a52d26ea09dd7340ce507", + "9c46b8765703273feb10a2ebd810e70b8e2ca44a", + "fb3e21cf45b04b617011d2b30973f3e5ce60d0cd", + ], + ) + .with_sorting(topo::Sorting::TopoOrder) + .check_with_predicate(|oid| oid != hex_to_id("eeab3243aad67bc838fc4425f759453bf0b47785")) + } + + #[test] + fn end_along_first_parent() -> crate::Result { + TraversalAssertion::new( + &["d09384f312b03e4a1413160739805ff25e8fe99d"], + &["33eb18340e4eaae3e3dcf80222b02f161cd3f966"], + &[ + "d09384f312b03e4a1413160739805ff25e8fe99d", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + ], + ) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } +} + +mod first_parent { + use gix_traverse::commit::{topo, Parents}; + + use super::TraversalAssertion; + + #[test] + fn basic() -> crate::Result { + let mut assertion = TraversalAssertion::new( + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &[], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + "f1cce1b5c7efcdfa106e95caa6c45a2cae48a481", + "945d8a360915631ad545e0cf04630d86d3d4eaa1", + "a863c02247a6c5ba32dff5224459f52aa7f77f7b", + "2f291881edfb0597493a52d26ea09dd7340ce507", + "9c46b8765703273feb10a2ebd810e70b8e2ca44a", + "fb3e21cf45b04b617011d2b30973f3e5ce60d0cd", + ], + ); + assertion + .with_parents(Parents::First) + .with_sorting(topo::Sorting::TopoOrder) + .check()?; + + assertion.assert_baseline("first-parent"); + Ok(()) + } + + #[test] + fn with_end() -> crate::Result { + TraversalAssertion::new( + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &["f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + ], + ) + .with_parents(Parents::First) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } + + #[test] + fn end_is_second_parent() -> crate::Result { + TraversalAssertion::new( + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &["3be0c4c793c634c8fd95054345d4935d10a0879a"], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + ], + ) + .with_parents(Parents::First) + .with_sorting(topo::Sorting::TopoOrder) + .check() + } +} + +mod date_order { + use gix_traverse::commit::topo; + + use super::TraversalAssertion; + + #[test] + fn simple() -> crate::Result { + TraversalAssertion::new( + // Same tip and end as basic::one_end() but the order should be + // different. + &["62ed296d9986f50477e9f7b7e81cd0258939a43d"], + &["f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"], + &[ + "62ed296d9986f50477e9f7b7e81cd0258939a43d", + "722bf6b8c3d9e3a11fa5100a02ed9b140e1d209c", + "3be0c4c793c634c8fd95054345d4935d10a0879a", + "2083b02a78e88b747e305b6ed3d5a861cf9fb73f", + "302a5d0530ec688c241f32c2f2b61b964dd17bee", + "d09384f312b03e4a1413160739805ff25e8fe99d", + "eeab3243aad67bc838fc4425f759453bf0b47785", + "22fbc169eeca3c9678fc7028aa80fad5ef49019f", + "693c775700cf90bd158ee6e7f14dd1b7bd83a4ce", + "33eb18340e4eaae3e3dcf80222b02f161cd3f966", + "1a27cb1a26c9faed9f0d1975326fe51123ab01ed", + ], + ) + .with_sorting(topo::Sorting::DateOrder) + .check() + } +} diff --git a/gix-traverse/tests/fixtures/generated-archives/make_repo_for_topo.tar.xz b/gix-traverse/tests/fixtures/generated-archives/make_repo_for_topo.tar.xz new file mode 100644 index 00000000000..12a8797c097 Binary files /dev/null and b/gix-traverse/tests/fixtures/generated-archives/make_repo_for_topo.tar.xz differ diff --git a/gix-traverse/tests/fixtures/make_repo_for_topo.sh b/gix-traverse/tests/fixtures/make_repo_for_topo.sh new file mode 100755 index 00000000000..6550da2e2c0 --- /dev/null +++ b/gix-traverse/tests/fixtures/make_repo_for_topo.sh @@ -0,0 +1,66 @@ +#!/bin/bash +set -eu -o pipefail + +function tick () { + if test -z "${tick+set}" + then + tick=1112911993 + else + tick=$(($tick + 60)) + fi + GIT_COMMITTER_DATE="$tick -0700" + GIT_AUTHOR_DATE="$tick -0700" + export GIT_COMMITTER_DATE GIT_AUTHOR_DATE +} + +tick +function commit() { + local message=${1:?first argument is the commit message} + tick + git commit --allow-empty -m "$message" +} + +function optimize() { + git commit-graph write --no-progress --reachable + git repack -adq +} + +function collect_baselines() { + git rev-list --topo-order HEAD > all-commits.baseline + git rev-list --topo-order --first-parent HEAD > first-parent.baseline +} + +git init +git config merge.ff false + +git checkout -q -b main +for i in {0..5}; do + commit c$i +done + +git branch branch1 +for i in {6..8}; do + commit c$i +done + +git checkout -q branch1 +commit b1c1 + +git checkout -q main +commit c9 + +git merge branch1 -m merge + +git checkout -q branch1 +commit c10 +commit c11 + +git checkout -q branch1 +commit b1c2 + +git checkout -q main +git merge branch1 -m merge +commit c12 + +optimize +collect_baselines diff --git a/gix-worktree/src/stack/platform.rs b/gix-worktree/src/stack/platform.rs index 0fcdf90f216..0b270516d24 100644 --- a/gix-worktree/src/stack/platform.rs +++ b/gix-worktree/src/stack/platform.rs @@ -20,6 +20,7 @@ impl<'a> Platform<'a> { /// # Panics /// /// If the cache was configured without exclude patterns. + #[doc(alias = "is_path_ignored", alias = "git2")] pub fn is_excluded(&self) -> bool { self.matching_exclude_pattern() .map_or(false, |m| !m.pattern.is_negative()) diff --git a/gix-worktree/src/stack/state/mod.rs b/gix-worktree/src/stack/state/mod.rs index ba6383fee85..04afb046368 100644 --- a/gix-worktree/src/stack/state/mod.rs +++ b/gix-worktree/src/stack/state/mod.rs @@ -131,7 +131,7 @@ impl State { // Stage 0 means there is no merge going on, stage 2 means it's 'our' side of the merge, but then // there won't be a stage 0. - if entry.mode == gix_index::entry::Mode::FILE && (entry.stage() == 0 || entry.stage() == 2) { + if entry.mode == gix_index::entry::Mode::FILE && (entry.stage_raw() == 0 || entry.stage_raw() == 2) { let basename = path.rfind_byte(b'/').map_or(path, |pos| path[pos + 1..].as_bstr()); let ignore_source = names.iter().find_map(|t| { match case { diff --git a/gix/src/attribute_stack.rs b/gix/src/attribute_stack.rs index 66497def6ae..e2b9ecc5ce6 100644 --- a/gix/src/attribute_stack.rs +++ b/gix/src/attribute_stack.rs @@ -39,6 +39,7 @@ impl<'repo> AttributeStack<'repo> { /// path is created as directory. If it's not known it is assumed to be a file. /// /// Provide access to cached information for that `relative` path via the returned platform. + #[doc(alias = "is_path_ignored", alias = "git2")] pub fn at_path( &mut self, relative: impl AsRef, diff --git a/gix/src/config/cache/util.rs b/gix/src/config/cache/util.rs index 4c1d6c69399..df120fb75d8 100644 --- a/gix/src/config/cache/util.rs +++ b/gix/src/config/cache/util.rs @@ -132,10 +132,6 @@ pub trait ApplyLeniency { fn with_leniency(self, is_lenient: bool) -> Self; } -pub trait IgnoreEmptyPath { - fn ignore_empty(self) -> Self; -} - pub trait ApplyLeniencyDefault { fn with_lenient_default(self, is_lenient: bool) -> Self; } @@ -154,16 +150,6 @@ impl ApplyLeniency for Result, E> { } } -impl IgnoreEmptyPath for Result>, gix_config::path::interpolate::Error> { - fn ignore_empty(self) -> Self { - match self { - Ok(maybe_path) => Ok(maybe_path), - Err(gix_config::path::interpolate::Error::Missing { .. }) => Ok(None), - Err(err) => Err(err), - } - } -} - impl ApplyLeniencyDefault for Result where T: Default, diff --git a/gix/src/config/snapshot/credential_helpers.rs b/gix/src/config/snapshot/credential_helpers.rs index fdac608f541..f84efa896e8 100644 --- a/gix/src/config/snapshot/credential_helpers.rs +++ b/gix/src/config/snapshot/credential_helpers.rs @@ -6,7 +6,6 @@ use crate::config::cache::util::ApplyLeniency; use crate::{ bstr::{ByteSlice, ByteVec}, config::{ - cache::util::IgnoreEmptyPath, tree::{credential, gitoxide::Credentials, Core, Credential, Key}, Snapshot, }, @@ -197,3 +196,17 @@ fn normalize(url: &mut gix_url::Url) { url.path.pop(); } } + +trait IgnoreEmptyPath { + fn ignore_empty(self) -> Self; +} + +impl IgnoreEmptyPath for Result>, gix_config::path::interpolate::Error> { + fn ignore_empty(self) -> Self { + match self { + Ok(maybe_path) => Ok(maybe_path), + Err(gix_config::path::interpolate::Error::Missing { .. }) => Ok(None), + Err(err) => Err(err), + } + } +} diff --git a/gix/src/ext/object_id.rs b/gix/src/ext/object_id.rs index d4d9467664b..d7f91164170 100644 --- a/gix/src/ext/object_id.rs +++ b/gix/src/ext/object_id.rs @@ -1,9 +1,8 @@ use gix_hash::ObjectId; -use gix_traverse::commit::{ancestors, Ancestors}; pub trait Sealed {} -pub type AncestorsIter = Ancestors bool, ancestors::State>; +pub type AncestorsIter = gix_traverse::commit::Simple bool>; /// An extension trait to add functionality to [`ObjectId`]s. pub trait ObjectIdExt: Sealed { @@ -23,7 +22,7 @@ impl ObjectIdExt for ObjectId { where Find: gix_object::Find, { - Ancestors::new(Some(self), ancestors::State::default(), find) + gix_traverse::commit::Simple::new(Some(self), find) } fn attach(self, repo: &crate::Repository) -> crate::Id<'_> { diff --git a/gix/src/remote/connection/fetch/update_refs/mod.rs b/gix/src/remote/connection/fetch/update_refs/mod.rs index 06a74fb3cbc..e5d3eac1b2d 100644 --- a/gix/src/remote/connection/fetch/update_refs/mod.rs +++ b/gix/src/remote/connection/fetch/update_refs/mod.rs @@ -162,7 +162,7 @@ pub(crate) fn update( .to_owned() .ancestors(&repo.objects) .sorting( - gix_traverse::commit::Sorting::ByCommitTimeNewestFirstCutoffOlderThan { + gix_traverse::commit::simple::Sorting::ByCommitTimeNewestFirstCutoffOlderThan { seconds: local_commit_time }, ) diff --git a/gix/src/repository/attributes.rs b/gix/src/repository/attributes.rs index 7f747f7fd98..06bcb569970 100644 --- a/gix/src/repository/attributes.rs +++ b/gix/src/repository/attributes.rs @@ -106,6 +106,7 @@ impl Repository { /// When only excludes are desired, this is the most efficient way to obtain them. Otherwise use /// [`Repository::attributes()`] for accessing both attributes and excludes. // TODO: test + #[doc(alias = "is_path_ignored", alias = "git2")] #[cfg(feature = "excludes")] pub fn excludes( &self, diff --git a/gix/src/repository/index.rs b/gix/src/repository/index.rs index 18899d09fba..9d2ba0ccfb1 100644 --- a/gix/src/repository/index.rs +++ b/gix/src/repository/index.rs @@ -40,6 +40,11 @@ impl crate::Repository { /// Return a shared worktree index which is updated automatically if the in-memory snapshot has become stale as the underlying file /// on disk has changed. /// + /// ### Notes + /// + /// * This will fail if the file doesn't exist, like in a newly initialized repository. If that is the case, use + /// [index_or_empty()](Self::index_or_empty) or [try_index()](Self::try_index) instead. + /// /// The index file is shared across all clones of this repository. pub fn index(&self) -> Result { self.try_index().and_then(|opt| match opt { diff --git a/gix/src/revision/spec/parse/delegate/navigate.rs b/gix/src/revision/spec/parse/delegate/navigate.rs index 731a24136d3..43f40654f40 100644 --- a/gix/src/revision/spec/parse/delegate/navigate.rs +++ b/gix/src/revision/spec/parse/delegate/navigate.rs @@ -1,11 +1,11 @@ use std::collections::HashSet; use gix_hash::ObjectId; +use gix_index::entry::Stage; use gix_revision::spec::parse::{ delegate, delegate::{PeelTo, Traversal}, }; -use gix_traverse::commit::Sorting; use crate::{ bstr::{BStr, ByteSlice}, @@ -192,7 +192,7 @@ impl<'repo> delegate::Navigate for Delegate<'repo> { match oid .attach(repo) .ancestors() - .sorting(Sorting::ByCommitTimeNewestFirst) + .sorting(gix_traverse::commit::simple::Sorting::ByCommitTimeNewestFirst) .all() { Ok(iter) => { @@ -241,15 +241,10 @@ impl<'repo> delegate::Navigate for Delegate<'repo> { references .peeled() .filter_map(Result::ok) - .filter(|r| { - r.id() - .object() - .ok() - .map_or(false, |obj| obj.kind == gix_object::Kind::Commit) - }) + .filter(|r| r.id().header().ok().map_or(false, |obj| obj.kind().is_commit())) .filter_map(|r| r.detach().peeled), ) - .sorting(Sorting::ByCommitTimeNewestFirst) + .sorting(gix_traverse::commit::simple::Sorting::ByCommitTimeNewestFirst) .all() { Ok(iter) => { @@ -305,9 +300,18 @@ impl<'repo> delegate::Navigate for Delegate<'repo> { } fn index_lookup(&mut self, path: &BStr, stage: u8) -> Option<()> { + let stage = match stage { + 0 => Stage::Unconflicted, + 1 => Stage::Base, + 2 => Stage::Ours, + 3 => Stage::Theirs, + _ => unreachable!( + "BUG: driver will not pass invalid stages (and it uses integer to avoid gix-index as dependency)" + ), + }; self.unset_disambiguate_call(); match self.repo.index() { - Ok(index) => match index.entry_by_path_and_stage(path, stage.into()) { + Ok(index) => match index.entry_by_path_and_stage(path, stage) { Some(entry) => { self.objs[self.idx] .get_or_insert_with(HashSet::default) @@ -323,21 +327,17 @@ impl<'repo> delegate::Navigate for Delegate<'repo> { Some(()) } None => { - let stage_hint = [0, 1, 2] + let stage_hint = [Stage::Unconflicted, Stage::Base, Stage::Ours] .iter() .filter(|our_stage| **our_stage != stage) - .find_map(|stage| { - index - .entry_index_by_path_and_stage(path, (*stage).into()) - .map(|_| (*stage).into()) - }); + .find_map(|stage| index.entry_index_by_path_and_stage(path, *stage).map(|_| *stage)); let exists = self .repo .work_dir() .map_or(false, |root| root.join(gix_path::from_bstr(path)).exists()); self.err.push(Error::IndexLookup { desired_path: path.into(), - desired_stage: stage.into(), + desired_stage: stage, exists, stage_hint, }); diff --git a/gix/src/revision/spec/parse/types.rs b/gix/src/revision/spec/parse/types.rs index 297ab5b467b..9629b0b34d4 100644 --- a/gix/src/revision/spec/parse/types.rs +++ b/gix/src/revision/spec/parse/types.rs @@ -98,7 +98,7 @@ pub enum Error { desired: usize, available: usize, }, - #[error("Path {desired_path:?} did not exist in index at stage {desired_stage}{}{}", stage_hint.map(|actual|format!(". It does exist at stage {actual}")).unwrap_or_default(), exists.then(|| ". It exists on disk").unwrap_or(". It does not exist on disk"))] + #[error("Path {desired_path:?} did not exist in index at stage {}{}{}", *desired_stage as u8, stage_hint.map(|actual|format!(". It does exist at stage {}", actual as u8)).unwrap_or_default(), exists.then(|| ". It exists on disk").unwrap_or(". It does not exist on disk"))] IndexLookup { desired_path: BString, desired_stage: gix_index::entry::Stage, @@ -184,7 +184,7 @@ pub enum Error { next: Option>, }, #[error(transparent)] - Traverse(#[from] gix_traverse::commit::ancestors::Error), + Traverse(#[from] gix_traverse::commit::simple::Error), #[error(transparent)] Walk(#[from] crate::revision::walk::Error), #[error("Spec does not contain a single object id")] diff --git a/gix/src/revision/walk.rs b/gix/src/revision/walk.rs index 19d15d569ab..dd7c5a8c1dc 100644 --- a/gix/src/revision/walk.rs +++ b/gix/src/revision/walk.rs @@ -8,7 +8,7 @@ use crate::{ext::ObjectIdExt, revision, Repository}; #[allow(missing_docs)] pub enum Error { #[error(transparent)] - AncestorIter(#[from] gix_traverse::commit::ancestors::Error), + SimpleTraversal(#[from] gix_traverse::commit::simple::Error), #[error(transparent)] ShallowCommits(#[from] crate::shallow::open::Error), #[error(transparent)] @@ -22,8 +22,8 @@ pub struct Info<'repo> { pub id: gix_hash::ObjectId, /// All parent ids we have encountered. Note that these will be at most one if [`Parents::First`][gix_traverse::commit::Parents::First] is enabled. pub parent_ids: gix_traverse::commit::ParentIds, - /// The time at which the commit was created. It's only `Some(_)` if sorting is not [`Sorting::BreadthFirst`][gix_traverse::commit::Sorting::BreadthFirst], - /// as the walk needs to require the commit-date. + /// The time at which the commit was created. It will only be `Some(_)` if the chosen traversal was + /// taking dates into consideration. pub commit_time: Option, repo: &'repo Repository, @@ -91,7 +91,7 @@ impl<'repo> Info<'repo> { pub struct Platform<'repo> { pub(crate) repo: &'repo Repository, pub(crate) tips: Vec, - pub(crate) sorting: gix_traverse::commit::Sorting, + pub(crate) sorting: gix_traverse::commit::simple::Sorting, pub(crate) parents: gix_traverse::commit::Parents, pub(crate) use_commit_graph: Option, pub(crate) commit_graph: Option, @@ -113,7 +113,7 @@ impl<'repo> Platform<'repo> { /// Create-time builder methods impl<'repo> Platform<'repo> { /// Set the sort mode for commits to the given value. The default is to order topologically breadth-first. - pub fn sorting(mut self, sorting: gix_traverse::commit::Sorting) -> Self { + pub fn sorting(mut self, sorting: gix_traverse::commit::simple::Sorting) -> Self { self.sorting = sorting; self } @@ -166,40 +166,35 @@ impl<'repo> Platform<'repo> { Ok(revision::Walk { repo, inner: Box::new( - gix_traverse::commit::Ancestors::filtered( - tips, - gix_traverse::commit::ancestors::State::default(), - &repo.objects, - { - // Note that specific shallow handling for commit-graphs isn't needed as these contain - // all information there is, and exclude shallow parents to be structurally consistent. - let shallow_commits = repo.shallow_commits()?; - let mut grafted_parents_to_skip = Vec::new(); - let mut buf = Vec::new(); - move |id| { - if !filter(id) { - return false; - } - match shallow_commits.as_ref() { - Some(commits) => { - let id = id.to_owned(); - if let Ok(idx) = grafted_parents_to_skip.binary_search(&id) { - grafted_parents_to_skip.remove(idx); - return false; - }; - if commits.binary_search(&id).is_ok() { - if let Ok(commit) = repo.objects.find_commit_iter(&id, &mut buf) { - grafted_parents_to_skip.extend(commit.parent_ids()); - grafted_parents_to_skip.sort(); - } - }; - true - } - None => true, + gix_traverse::commit::Simple::filtered(tips, &repo.objects, { + // Note that specific shallow handling for commit-graphs isn't needed as these contain + // all information there is, and exclude shallow parents to be structurally consistent. + let shallow_commits = repo.shallow_commits()?; + let mut grafted_parents_to_skip = Vec::new(); + let mut buf = Vec::new(); + move |id| { + if !filter(id) { + return false; + } + match shallow_commits.as_ref() { + Some(commits) => { + let id = id.to_owned(); + if let Ok(idx) = grafted_parents_to_skip.binary_search(&id) { + grafted_parents_to_skip.remove(idx); + return false; + }; + if commits.binary_search(&id).is_ok() { + if let Ok(commit) = repo.objects.find_commit_iter(&id, &mut buf) { + grafted_parents_to_skip.extend(commit.parent_ids()); + grafted_parents_to_skip.sort(); + } + }; + true } + None => true, } - }, - ) + } + }) .sorting(sorting)? .parents(parents) .commit_graph( @@ -226,13 +221,12 @@ pub(crate) mod iter { /// The iterator returned by [`crate::revision::walk::Platform::all()`]. pub struct Walk<'repo> { pub(crate) repo: &'repo crate::Repository, - pub(crate) inner: Box< - dyn Iterator> + 'repo, - >, + pub(crate) inner: + Box> + 'repo>, } impl<'repo> Iterator for Walk<'repo> { - type Item = Result, gix_traverse::commit::ancestors::Error>; + type Item = Result, gix_traverse::commit::simple::Error>; fn next(&mut self) -> Option { self.inner diff --git a/gix/src/status/index_worktree.rs b/gix/src/status/index_worktree.rs index d95ab5a2438..c658e0e2ee2 100644 --- a/gix/src/status/index_worktree.rs +++ b/gix/src/status/index_worktree.rs @@ -199,10 +199,7 @@ mod submodule_status { let local_repo = repo.to_thread_local(); let submodule_paths = match local_repo.submodules()? { Some(sm) => { - let mut v: Vec<_> = sm - .filter(|sm| sm.is_active().unwrap_or_default()) - .filter_map(|sm| sm.path().ok().map(Cow::into_owned)) - .collect(); + let mut v: Vec<_> = sm.filter_map(|sm| sm.path().ok().map(Cow::into_owned)).collect(); v.sort(); v } @@ -271,7 +268,8 @@ mod submodule_status { /// /// ### Submodules /// -/// Note that submodules can be set to 'inactive' which automatically excludes them from the status operation. +/// Note that submodules can be set to 'inactive', which will not exclude them from the status operation, similar to +/// how `git status` includes them. /// /// ### Index Changes /// @@ -613,7 +611,10 @@ pub mod iter { /// /// * `patterns` /// - Optional patterns to use to limit the paths to look at. If empty, all paths are considered. - pub fn into_index_worktree_iter(self, patterns: Vec) -> Result { + pub fn into_index_worktree_iter( + self, + patterns: impl IntoIterator, + ) -> Result { let index = match self.index { None => IndexPersistedOrInMemory::Persisted(self.repo.index_or_empty()?), Some(index) => index, @@ -634,6 +635,7 @@ pub mod iter { { let (tx, rx) = std::sync::mpsc::channel(); let mut collect = Collect { tx }; + let patterns: Vec<_> = patterns.into_iter().collect(); let join = std::thread::Builder::new() .name("gix::status::index_worktree::iter::producer".into()) .spawn({ diff --git a/gix/tests/id/mod.rs b/gix/tests/id/mod.rs index f284a76e65a..143881d92c9 100644 --- a/gix/tests/id/mod.rs +++ b/gix/tests/id/mod.rs @@ -87,7 +87,7 @@ mod ancestors { let commits_by_commit_date = head .ancestors() .use_commit_graph(!use_commit_graph) - .sorting(commit::Sorting::ByCommitTimeNewestFirst) + .sorting(commit::simple::Sorting::ByCommitTimeNewestFirst) .all()? .map(|c| c.map(gix::revision::walk::Info::detach)) .collect::, _>>()?; @@ -121,7 +121,7 @@ mod ancestors { let head = repo.head()?.into_peeled_id()?; let commits = head .ancestors() - .sorting(commit::Sorting::ByCommitTimeNewestFirst) // assure we have time set + .sorting(commit::simple::Sorting::ByCommitTimeNewestFirst) // assure we have time set .use_commit_graph(use_commit_graph) .all()? .collect::, _>>()?; @@ -141,9 +141,9 @@ mod ancestors { for use_commit_graph in [false, true] { for sorting in [ - commit::Sorting::BreadthFirst, - commit::Sorting::ByCommitTimeNewestFirst, - commit::Sorting::ByCommitTimeNewestFirstCutoffOlderThan { seconds: 0 }, + commit::simple::Sorting::BreadthFirst, + commit::simple::Sorting::ByCommitTimeNewestFirst, + commit::simple::Sorting::ByCommitTimeNewestFirstCutoffOlderThan { seconds: 0 }, ] { let commits_graph_order = head .ancestors() diff --git a/gix/tests/repository/config/mod.rs b/gix/tests/repository/config/mod.rs index 056bc903546..227a7ed308e 100644 --- a/gix/tests/repository/config/mod.rs +++ b/gix/tests/repository/config/mod.rs @@ -38,7 +38,6 @@ mod ssh_options { #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] mod transport_options; -#[cfg(feature = "blocking-network-client")] #[cfg(feature = "blocking-network-client")] pub fn repo(name: &str) -> gix::Repository { repo_opts(name, |opts| opts.strict_config(true)) diff --git a/gix/tests/repository/shallow.rs b/gix/tests/repository/shallow.rs index aac1ee5cc37..8e5c2bd0a66 100644 --- a/gix/tests/repository/shallow.rs +++ b/gix/tests/repository/shallow.rs @@ -44,7 +44,7 @@ fn yes() -> crate::Result { } mod traverse { - use gix_traverse::commit::Sorting; + use gix_traverse::commit::simple::Sorting; use serial_test::parallel; use crate::util::{hex_to_id, named_subrepo_opts};