From 8fe461281842b58aa11437445637c6e587bedd63 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 21 Sep 2021 20:42:08 +0800 Subject: [PATCH] refactor: split data::output::count::objects into files(#67) --- .../src/command/changelog/commit/history.rs | 3 +- .../src/command/changelog/commit/message.rs | 2 +- git-pack/src/cache/object.rs | 3 +- git-pack/src/data/output/count/mod.rs | 9 +- git-pack/src/data/output/count/objects.rs | 750 ------------------ git-pack/src/data/output/count/objects/mod.rs | 431 ++++++++++ .../src/data/output/count/objects/reduce.rs | 50 ++ .../src/data/output/count/objects/tree.rs | 114 +++ .../src/data/output/count/objects/types.rs | 115 +++ .../src/data/output/count/objects/util.rs | 48 ++ git-repository/src/easy/ext/object.rs | 2 +- git-repository/src/easy/oid.rs | 2 +- gitoxide-core/src/pack/create.rs | 3 +- 13 files changed, 773 insertions(+), 759 deletions(-) delete mode 100644 git-pack/src/data/output/count/objects.rs create mode 100644 git-pack/src/data/output/count/objects/mod.rs create mode 100644 git-pack/src/data/output/count/objects/reduce.rs create mode 100644 git-pack/src/data/output/count/objects/tree.rs create mode 100644 git-pack/src/data/output/count/objects/types.rs create mode 100644 git-pack/src/data/output/count/objects/util.rs diff --git a/cargo-smart-release/src/command/changelog/commit/history.rs b/cargo-smart-release/src/command/changelog/commit/history.rs index eae2b073159..8e73ddf14b1 100644 --- a/cargo-smart-release/src/command/changelog/commit/history.rs +++ b/cargo-smart-release/src/command/changelog/commit/history.rs @@ -1,6 +1,7 @@ -use crate::command::changelog_impl::commit::Message; use git_repository as git; +use crate::command::changelog_impl::commit::Message; + /// A head reference will all commits that are 'governed' by it, that is are in its exclusive ancestry. pub struct Segment<'a> { pub _head: git::refs::Reference, diff --git a/cargo-smart-release/src/command/changelog/commit/message.rs b/cargo-smart-release/src/command/changelog/commit/message.rs index f6640feba93..1ddbb1aede9 100644 --- a/cargo-smart-release/src/command/changelog/commit/message.rs +++ b/cargo-smart-release/src/command/changelog/commit/message.rs @@ -1,8 +1,8 @@ +use git_conventional::Type; use git_repository as git; use git_repository::bstr::{BStr, ByteSlice}; use crate::command::changelog_impl::commit::Message; -use git_conventional::Type; #[derive(Debug)] #[cfg_attr(test, derive(PartialEq, Eq))] diff --git a/git-pack/src/cache/object.rs b/git-pack/src/cache/object.rs index 6b32a7477f3..a59e46799b3 100644 --- a/git-pack/src/cache/object.rs +++ b/git-pack/src/cache/object.rs @@ -8,9 +8,10 @@ use crate::cache; mod memory { use std::num::NonZeroUsize; - use crate::cache; use clru::WeightScale; + use crate::cache; + struct Entry { data: Vec, kind: git_object::Kind, diff --git a/git-pack/src/data/output/count/mod.rs b/git-pack/src/data/output/count/mod.rs index 345e13c01f6..556e9e6d458 100644 --- a/git-pack/src/data/output/count/mod.rs +++ b/git-pack/src/data/output/count/mod.rs @@ -39,6 +39,11 @@ impl Count { } } +#[path = "objects/mod.rs"] +mod objects_impl; +pub use objects_impl::{objects, objects_unthreaded}; + /// -pub mod objects; -pub use objects::{objects, objects_unthreaded}; +pub mod objects { + pub use super::objects_impl::{Error, ObjectExpansion, Options, Outcome, Result}; +} diff --git a/git-pack/src/data/output/count/objects.rs b/git-pack/src/data/output/count/objects.rs deleted file mode 100644 index aa6e964d3c3..00000000000 --- a/git-pack/src/data/output/count/objects.rs +++ /dev/null @@ -1,750 +0,0 @@ -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; - -use git_features::{parallel, progress::Progress}; -use git_hash::{oid, ObjectId}; -use git_object::{CommitRefIter, TagRefIter}; - -use crate::{data::output, find, FindExt}; - -/// The return type used by [`objects()`]. -pub type Result = std::result::Result<(Vec, Outcome), Error>; - -/// Generate [`Count`][output::Count]s from input `objects` with object expansion based on [`options`][Options] -/// to learn which objects would would constitute a pack. This step is required to know exactly how many objects would -/// be in a pack while keeping data around to avoid minimize object database access. -/// -/// A [`Count`][output::Count] object maintains enough state to greatly accelerate future access of packed objects. -/// -/// * `db` - the object store to use for accessing objects. -/// * `make_cache` - a function to create thread-local pack caches -/// * `objects_ids` -/// * A list of objects ids to add to the pack. Duplication checks are performed so no object is ever added to a pack twice. -/// * Objects may be expanded based on the provided [`options`][Options] -/// * `progress` -/// * a way to obtain progress information -/// * `should_interrupt` -/// * A flag that is set to true if the operation should stop -/// * `options` -/// * more configuration -pub fn objects( - db: Find, - make_cache: impl Fn() -> Cache + Send + Sync, - objects_ids: Iter, - progress: impl Progress, - should_interrupt: &AtomicBool, - Options { - thread_limit, - input_object_expansion, - chunk_size, - #[cfg(feature = "object-cache-dynamic")] - object_cache_size_in_bytes, - }: Options, -) -> Result, IterErr> -where - Find: crate::Find + Send + Sync, - ::Error: Send, - Iter: Iterator> + Send, - Oid: Into + Send, - IterErr: std::error::Error + Send, - Cache: crate::cache::DecodeEntry, -{ - let lower_bound = objects_ids.size_hint().0; - let (chunk_size, thread_limit, _) = parallel::optimize_chunk_size_and_thread_limit( - chunk_size, - if lower_bound == 0 { None } else { Some(lower_bound) }, - thread_limit, - None, - ); - let chunks = util::Chunks { - iter: objects_ids, - size: chunk_size, - }; - let seen_objs = dashmap::DashSet::::new(); - let progress = Arc::new(parking_lot::Mutex::new(progress)); - - parallel::in_parallel( - chunks, - thread_limit, - { - let progress = Arc::clone(&progress); - move |n| { - ( - Vec::new(), // object data buffer - Vec::new(), // object data buffer 2 to hold two objects at a time - make_cache(), // cache to speed up pack operations - { - let mut p = progress.lock().add_child(format!("thread {}", n)); - p.init(None, git_features::progress::count("objects")); - p - }, - ) - } - }, - { - move |oids: Vec>, (buf1, buf2, cache, progress)| { - expand_inner( - &db, - input_object_expansion, - &seen_objs, - oids, - buf1, - buf2, - cache, - progress, - should_interrupt, - true, - #[cfg(feature = "object-cache-dynamic")] - object_cache_size_in_bytes, - ) - } - }, - reduce::Statistics::new(progress), - ) -} - -/// Like [`objects()`] but using a single thread only to mostly save on the otherwise required overhead. -pub fn objects_unthreaded( - db: Find, - pack_cache: &mut impl crate::cache::DecodeEntry, - object_ids: impl Iterator>, - mut progress: impl Progress, - should_interrupt: &AtomicBool, - input_object_expansion: ObjectExpansion, - #[cfg(feature = "object-cache-dynamic")] object_cache_size_in_bytes: usize, -) -> Result, IterErr> -where - Find: crate::Find + Send + Sync, - Oid: Into + Send, - IterErr: std::error::Error + Send, -{ - let seen_objs = RefCell::new(HashSet::::new()); - - let (mut buf1, mut buf2) = (Vec::new(), Vec::new()); - expand_inner( - &db, - input_object_expansion, - &seen_objs, - object_ids, - &mut buf1, - &mut buf2, - pack_cache, - &mut progress, - should_interrupt, - false, - #[cfg(feature = "object-cache-dynamic")] - object_cache_size_in_bytes, - ) -} - -#[allow(clippy::too_many_arguments)] -fn expand_inner( - db: &Find, - input_object_expansion: ObjectExpansion, - seen_objs: &impl util::InsertImmutable, - oids: impl IntoIterator>, - buf1: &mut Vec, - buf2: &mut Vec, - cache: &mut impl crate::cache::DecodeEntry, - progress: &mut impl Progress, - should_interrupt: &AtomicBool, - allow_pack_lookups: bool, - #[cfg(feature = "object-cache-dynamic")] object_cache_size_in_bytes: usize, -) -> Result, IterErr> -where - Find: crate::Find + Send + Sync, - Oid: Into + Send, - IterErr: std::error::Error + Send, -{ - use ObjectExpansion::*; - - let mut out = Vec::new(); - let mut tree_traversal_state = git_traverse::tree::breadthfirst::State::default(); - let mut tree_diff_state = git_diff::tree::State::default(); - let mut parent_commit_ids = Vec::new(); - let mut traverse_delegate = tree::traverse::AllUnseen::new(seen_objs); - let mut changes_delegate = tree::changes::AllNew::new(seen_objs); - let mut outcome = Outcome::default(); - #[cfg(feature = "object-cache-dynamic")] - let mut obj_cache = crate::cache::object::MemoryCappedHashmap::new(object_cache_size_in_bytes); - #[cfg(not(feature = "object-cache-dynamic"))] - let mut obj_cache = crate::cache::object::Never; - - let stats = &mut outcome; - for id in oids.into_iter() { - if should_interrupt.load(Ordering::Relaxed) { - return Err(Error::Interrupted); - } - - let id = id.map(|oid| oid.into()).map_err(Error::InputIteration)?; - let obj = db.find(id, buf1, cache)?; - stats.input_objects += 1; - match input_object_expansion { - TreeAdditionsComparedToAncestor => { - use git_object::Kind::*; - let mut obj = obj; - let mut id = id.to_owned(); - - loop { - push_obj_count_unique(&mut out, seen_objs, &id, &obj, progress, stats, false); - match obj.kind { - Tree | Blob => break, - Tag => { - id = TagRefIter::from_bytes(obj.data) - .target_id() - .expect("every tag has a target"); - obj = db.find(id, buf1, cache)?; - stats.expanded_objects += 1; - continue; - } - Commit => { - let current_tree_iter = { - let mut commit_iter = CommitRefIter::from_bytes(obj.data); - let tree_id = commit_iter.tree_id().expect("every commit has a tree"); - parent_commit_ids.clear(); - for token in commit_iter { - match token { - Ok(git_object::commit::ref_iter::Token::Parent { id }) => { - parent_commit_ids.push(id) - } - Ok(_) => break, - Err(err) => return Err(Error::CommitDecode(err)), - } - } - let obj = db.find(tree_id, buf1, cache)?; - push_obj_count_unique(&mut out, seen_objs, &tree_id, &obj, progress, stats, true); - git_object::TreeRefIter::from_bytes(obj.data) - }; - - let objects = if parent_commit_ids.is_empty() { - traverse_delegate.clear(); - git_traverse::tree::breadthfirst( - current_tree_iter, - &mut tree_traversal_state, - |oid, buf| { - stats.decoded_objects += 1; - match db.find(oid, buf, cache).ok() { - Some(obj) => { - progress.inc(); - stats.expanded_objects += 1; - out.push(output::Count::from_data(oid, &obj)); - obj.try_into_tree_iter() - } - None => None, - } - }, - &mut traverse_delegate, - ) - .map_err(Error::TreeTraverse)?; - &traverse_delegate.non_trees - } else { - for commit_id in &parent_commit_ids { - let parent_tree_id = { - let parent_commit_obj = db.find(commit_id, buf2, cache)?; - - push_obj_count_unique( - &mut out, - seen_objs, - commit_id, - &parent_commit_obj, - progress, - stats, - true, - ); - CommitRefIter::from_bytes(parent_commit_obj.data) - .tree_id() - .expect("every commit has a tree") - }; - let parent_tree = { - let parent_tree_obj = db.find(parent_tree_id, buf2, cache)?; - push_obj_count_unique( - &mut out, - seen_objs, - &parent_tree_id, - &parent_tree_obj, - progress, - stats, - true, - ); - git_object::TreeRefIter::from_bytes(parent_tree_obj.data) - }; - - changes_delegate.clear(); - git_diff::tree::Changes::from(Some(parent_tree)) - .needed_to_obtain( - current_tree_iter.clone(), - &mut tree_diff_state, - |oid, buf| { - stats.decoded_objects += 1; - let id = oid.to_owned(); - match obj_cache.get(&id, buf) { - Some(_kind) => git_object::TreeRefIter::from_bytes(buf).into(), - None => match db.find_tree_iter(oid, buf, cache).ok() { - Some(_) => { - obj_cache.put(id, git_object::Kind::Tree, buf); - git_object::TreeRefIter::from_bytes(buf).into() - } - None => None, - }, - } - }, - &mut changes_delegate, - ) - .map_err(Error::TreeChanges)?; - } - &changes_delegate.objects - }; - for id in objects.iter() { - out.push(id_to_count(db, buf2, id, progress, stats, allow_pack_lookups)); - } - break; - } - } - } - } - TreeContents => { - use git_object::Kind::*; - let mut id = id; - let mut obj = obj; - loop { - push_obj_count_unique(&mut out, seen_objs, &id, &obj, progress, stats, false); - match obj.kind { - Tree => { - traverse_delegate.clear(); - git_traverse::tree::breadthfirst( - git_object::TreeRefIter::from_bytes(obj.data), - &mut tree_traversal_state, - |oid, buf| { - stats.decoded_objects += 1; - match db.find(oid, buf, cache).ok() { - Some(obj) => { - progress.inc(); - stats.expanded_objects += 1; - out.push(output::Count::from_data(oid, &obj)); - obj.try_into_tree_iter() - } - None => None, - } - }, - &mut traverse_delegate, - ) - .map_err(Error::TreeTraverse)?; - for id in traverse_delegate.non_trees.iter() { - out.push(id_to_count(db, buf1, id, progress, stats, allow_pack_lookups)); - } - break; - } - Commit => { - id = CommitRefIter::from_bytes(obj.data) - .tree_id() - .expect("every commit has a tree"); - stats.expanded_objects += 1; - obj = db.find(id, buf1, cache)?; - continue; - } - Blob => break, - Tag => { - id = TagRefIter::from_bytes(obj.data) - .target_id() - .expect("every tag has a target"); - stats.expanded_objects += 1; - obj = db.find(id, buf1, cache)?; - continue; - } - } - } - } - AsIs => push_obj_count_unique(&mut out, seen_objs, &id, &obj, progress, stats, false), - } - } - Ok((out, outcome)) -} - -mod tree { - pub mod changes { - use git_diff::tree::{ - visit::{Action, Change}, - Visit, - }; - use git_hash::ObjectId; - use git_object::bstr::BStr; - - use crate::data::output::count::objects::util::InsertImmutable; - - pub struct AllNew<'a, H> { - pub objects: Vec, - all_seen: &'a H, - } - - impl<'a, H> AllNew<'a, H> - where - H: InsertImmutable, - { - pub fn new(all_seen: &'a H) -> Self { - AllNew { - objects: Default::default(), - all_seen, - } - } - pub fn clear(&mut self) { - self.objects.clear(); - } - } - - impl<'a, H> Visit for AllNew<'a, H> - where - H: InsertImmutable, - { - fn pop_front_tracked_path_and_set_current(&mut self) {} - - fn push_back_tracked_path_component(&mut self, _component: &BStr) {} - - fn push_path_component(&mut self, _component: &BStr) {} - - fn pop_path_component(&mut self) {} - - fn visit(&mut self, change: Change) -> Action { - match change { - Change::Addition { oid, .. } | Change::Modification { oid, .. } => { - let inserted = self.all_seen.insert(oid); - if inserted { - self.objects.push(oid); - } - } - Change::Deletion { .. } => {} - }; - Action::Continue - } - } - } - - pub mod traverse { - use git_hash::ObjectId; - use git_object::{bstr::BStr, tree::EntryRef}; - use git_traverse::tree::{visit::Action, Visit}; - - use crate::data::output::count::objects::util::InsertImmutable; - - pub struct AllUnseen<'a, H> { - pub non_trees: Vec, - all_seen: &'a H, - } - - impl<'a, H> AllUnseen<'a, H> - where - H: InsertImmutable, - { - pub fn new(all_seen: &'a H) -> Self { - AllUnseen { - non_trees: Default::default(), - all_seen, - } - } - pub fn clear(&mut self) { - self.non_trees.clear(); - } - } - - impl<'a, H> Visit for AllUnseen<'a, H> - where - H: InsertImmutable, - { - fn pop_front_tracked_path_and_set_current(&mut self) {} - - fn push_back_tracked_path_component(&mut self, _component: &BStr) {} - - fn push_path_component(&mut self, _component: &BStr) {} - - fn pop_path_component(&mut self) {} - - fn visit_tree(&mut self, entry: &EntryRef<'_>) -> Action { - let inserted = self.all_seen.insert(entry.oid.to_owned()); - if inserted { - Action::Continue - } else { - Action::Skip - } - } - - fn visit_nontree(&mut self, entry: &EntryRef<'_>) -> Action { - let inserted = self.all_seen.insert(entry.oid.to_owned()); - if inserted { - self.non_trees.push(entry.oid.to_owned()); - } - Action::Continue - } - } - } -} - -#[inline] -fn push_obj_count_unique( - out: &mut Vec, - all_seen: &impl util::InsertImmutable, - id: &oid, - obj: &crate::data::Object<'_>, - progress: &mut impl Progress, - statistics: &mut Outcome, - count_expanded: bool, -) { - let inserted = all_seen.insert(id.to_owned()); - if inserted { - progress.inc(); - statistics.decoded_objects += 1; - if count_expanded { - statistics.expanded_objects += 1; - } - out.push(output::Count::from_data(id, obj)); - } -} - -#[inline] -fn id_to_count( - db: &Find, - buf: &mut Vec, - id: &oid, - progress: &mut impl Progress, - statistics: &mut Outcome, - allow_pack_lookups: bool, -) -> output::Count { - progress.inc(); - statistics.expanded_objects += 1; - output::Count { - id: id.to_owned(), - entry_pack_location: if allow_pack_lookups { - PackLocation::LookedUp(db.location_by_oid(id, buf)) - } else { - PackLocation::NotLookedUp - }, - } -} - -mod util { - pub trait InsertImmutable { - fn insert(&self, item: Item) -> bool; - } - - mod trait_impls { - use std::{cell::RefCell, collections::HashSet, hash::Hash}; - - use dashmap::DashSet; - - use super::InsertImmutable; - - impl InsertImmutable for DashSet { - fn insert(&self, item: T) -> bool { - self.insert(item) - } - } - - impl InsertImmutable for RefCell> { - fn insert(&self, item: T) -> bool { - self.borrow_mut().insert(item) - } - } - } - - pub struct Chunks { - pub size: usize, - pub iter: I, - } - - impl Iterator for Chunks - where - I: Iterator, - { - type Item = Vec; - - fn next(&mut self) -> Option { - let mut res = Vec::with_capacity(self.size); - let mut items_left = self.size; - for item in &mut self.iter { - res.push(item); - items_left -= 1; - if items_left == 0 { - break; - } - } - (!res.is_empty()).then(|| res) - } - } -} - -mod types { - /// Information gathered during the run of [`iter_from_objects()`][super::objects()]. - #[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] - #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] - pub struct Outcome { - /// The amount of objects provided to start the iteration. - pub input_objects: usize, - /// The amount of objects that have been expanded from the input source. - /// It's desirable to do that as expansion happens on multiple threads, allowing the amount of input objects to be small. - /// `expanded_objects - decoded_objects` is the 'cheap' object we found without decoding the object itself. - pub expanded_objects: usize, - /// The amount of fully decoded objects. These are the most expensive as they are fully decoded - pub decoded_objects: usize, - /// The total amount of encountered objects. Should be `expanded_objects + input_objects`. - pub total_objects: usize, - } - - impl Outcome { - pub(in crate::data::output::count) fn aggregate( - &mut self, - Outcome { - input_objects, - decoded_objects, - expanded_objects, - total_objects, - }: Self, - ) { - self.input_objects += input_objects; - self.decoded_objects += decoded_objects; - self.expanded_objects += expanded_objects; - self.total_objects += total_objects; - } - } - - /// The way input objects are handled - #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] - #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] - pub enum ObjectExpansion { - /// Don't do anything with the input objects except for transforming them into pack entries - AsIs, - /// If the input object is a Commit then turn it into a pack entry. Additionally obtain its tree, turn it into a pack entry - /// along with all of its contents, that is nested trees, and any other objects reachable from it. - /// Otherwise, the same as [`AsIs`][ObjectExpansion::AsIs]. - /// - /// This mode is useful if all reachable objects should be added, as in cloning a repository. - TreeContents, - /// If the input is a commit, obtain its ancestors and turn them into pack entries. Obtain the ancestor trees along with the commits - /// tree and turn them into pack entries. Finally obtain the added/changed objects when comparing the ancestor trees with the - /// current tree and turn them into entries as well. - /// Otherwise, the same as [`AsIs`][ObjectExpansion::AsIs]. - /// - /// This mode is useful to build a pack containing only new objects compared to a previous state. - TreeAdditionsComparedToAncestor, - } - - impl Default for ObjectExpansion { - fn default() -> Self { - ObjectExpansion::AsIs - } - } - - /// Configuration options for the pack generation functions provied in [this module][crate::data::output]. - #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] - #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] - pub struct Options { - /// The amount of threads to use at most when resolving the pack. If `None`, all logical cores are used. - /// If more than one thread is used, the order of returned [counts][crate::data::output::Count] is not deterministic anymore - /// especially when tree traversal is involved. Thus deterministic ordering requires `Some(1)` to be set. - pub thread_limit: Option, - /// The amount of objects per chunk or unit of work to be sent to threads for processing - pub chunk_size: usize, - /// The way input objects are handled - pub input_object_expansion: ObjectExpansion, - /// The size of a per-thread object cache in bytes to accelerate tree diffs in conjunction - /// with [ObjectExpansion::TreeAdditionsComparedToAncestor]. - /// - /// If zero, the cache is disabled but in a costly way. Consider using a low value instead. - /// - /// Defaults to 10 megabytes which usually leads to 2.5x speedups. - #[cfg(feature = "object-cache-dynamic")] - pub object_cache_size_in_bytes: usize, - } - - impl Default for Options { - fn default() -> Self { - Options { - thread_limit: None, - chunk_size: 10, - input_object_expansion: Default::default(), - #[cfg(feature = "object-cache-dynamic")] - object_cache_size_in_bytes: 10 * 1024 * 1024, - } - } - } - - /// The error returned by the pack generation iterator [bytes::FromEntriesIter][crate::data::output::bytes::FromEntriesIter]. - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error - where - FindErr: std::error::Error + 'static, - IterErr: std::error::Error + 'static, - { - #[error(transparent)] - CommitDecode(git_object::decode::Error), - #[error(transparent)] - FindExisting(#[from] FindErr), - #[error(transparent)] - InputIteration(IterErr), - #[error(transparent)] - TreeTraverse(git_traverse::tree::breadthfirst::Error), - #[error(transparent)] - TreeChanges(git_diff::tree::changes::Error), - #[error("Operation interrupted")] - Interrupted, - } -} -use std::{cell::RefCell, collections::HashSet}; - -pub use types::{Error, ObjectExpansion, Options, Outcome}; - -use crate::cache::Object; -use crate::data::output::count::PackLocation; - -mod reduce { - use std::{marker::PhantomData, sync::Arc}; - - use git_features::{parallel, progress::Progress}; - - use super::Outcome; - use crate::data::output; - - pub struct Statistics { - total: Outcome, - counts: Vec, - progress: Arc>, - _err: PhantomData, - } - - impl Statistics - where - P: Progress, - { - pub fn new(progress: Arc>) -> Self { - Statistics { - total: Default::default(), - counts: Default::default(), - progress, - _err: PhantomData::default(), - } - } - } - - impl parallel::Reduce for Statistics - where - P: Progress, - { - type Input = Result<(Vec, Outcome), E>; - type FeedProduce = (); - type Output = (Vec, Outcome); - type Error = E; - - fn feed(&mut self, item: Self::Input) -> Result { - let (counts, mut stats) = item?; - stats.total_objects = counts.len(); - self.total.aggregate(stats); - self.progress.lock().inc_by(counts.len()); - self.counts.extend(counts); - Ok(()) - } - - fn finalize(self) -> Result { - Ok((self.counts, self.total)) - } - } -} diff --git a/git-pack/src/data/output/count/objects/mod.rs b/git-pack/src/data/output/count/objects/mod.rs new file mode 100644 index 00000000000..2fc67a230db --- /dev/null +++ b/git-pack/src/data/output/count/objects/mod.rs @@ -0,0 +1,431 @@ +use std::{ + cell::RefCell, + collections::HashSet, + sync::{atomic::AtomicBool, Arc}, +}; + +use git_features::{parallel, progress::Progress}; +use git_hash::ObjectId; + +use crate::{data::output, find}; + +pub(in crate::data::output::count::objects_impl) mod reduce; +mod util; + +mod types; +pub use types::{Error, ObjectExpansion, Options, Outcome}; +mod tree; + +/// The return type used by [`objects()`]. +pub type Result = std::result::Result<(Vec, Outcome), Error>; + +/// Generate [`Count`][output::Count]s from input `objects` with object expansion based on [`options`][Options] +/// to learn which objects would would constitute a pack. This step is required to know exactly how many objects would +/// be in a pack while keeping data around to avoid minimize object database access. +/// +/// A [`Count`][output::Count] object maintains enough state to greatly accelerate future access of packed objects. +/// +/// * `db` - the object store to use for accessing objects. +/// * `make_cache` - a function to create thread-local pack caches +/// * `objects_ids` +/// * A list of objects ids to add to the pack. Duplication checks are performed so no object is ever added to a pack twice. +/// * Objects may be expanded based on the provided [`options`][Options] +/// * `progress` +/// * a way to obtain progress information +/// * `should_interrupt` +/// * A flag that is set to true if the operation should stop +/// * `options` +/// * more configuration +pub fn objects( + db: Find, + make_cache: impl Fn() -> Cache + Send + Sync, + objects_ids: Iter, + progress: impl Progress, + should_interrupt: &AtomicBool, + Options { + thread_limit, + input_object_expansion, + chunk_size, + #[cfg(feature = "object-cache-dynamic")] + object_cache_size_in_bytes, + }: Options, +) -> Result, IterErr> +where + Find: crate::Find + Send + Sync, + ::Error: Send, + Iter: Iterator> + Send, + Oid: Into + Send, + IterErr: std::error::Error + Send, + Cache: crate::cache::DecodeEntry, +{ + let lower_bound = objects_ids.size_hint().0; + let (chunk_size, thread_limit, _) = parallel::optimize_chunk_size_and_thread_limit( + chunk_size, + if lower_bound == 0 { None } else { Some(lower_bound) }, + thread_limit, + None, + ); + let chunks = util::Chunks { + iter: objects_ids, + size: chunk_size, + }; + let seen_objs = dashmap::DashSet::::new(); + let progress = Arc::new(parking_lot::Mutex::new(progress)); + + parallel::in_parallel( + chunks, + thread_limit, + { + let progress = Arc::clone(&progress); + move |n| { + ( + Vec::new(), // object data buffer + Vec::new(), // object data buffer 2 to hold two objects at a time + make_cache(), // cache to speed up pack operations + { + let mut p = progress.lock().add_child(format!("thread {}", n)); + p.init(None, git_features::progress::count("objects")); + p + }, + ) + } + }, + { + move |oids: Vec>, (buf1, buf2, cache, progress)| { + expand::this( + &db, + input_object_expansion, + &seen_objs, + oids, + buf1, + buf2, + cache, + progress, + should_interrupt, + true, + #[cfg(feature = "object-cache-dynamic")] + object_cache_size_in_bytes, + ) + } + }, + reduce::Statistics::new(progress), + ) +} + +/// Like [`objects()`] but using a single thread only to mostly save on the otherwise required overhead. +pub fn objects_unthreaded( + db: Find, + pack_cache: &mut impl crate::cache::DecodeEntry, + object_ids: impl Iterator>, + mut progress: impl Progress, + should_interrupt: &AtomicBool, + input_object_expansion: ObjectExpansion, + #[cfg(feature = "object-cache-dynamic")] object_cache_size_in_bytes: usize, +) -> Result, IterErr> +where + Find: crate::Find + Send + Sync, + Oid: Into + Send, + IterErr: std::error::Error + Send, +{ + let seen_objs = RefCell::new(HashSet::::new()); + + let (mut buf1, mut buf2) = (Vec::new(), Vec::new()); + expand::this( + &db, + input_object_expansion, + &seen_objs, + object_ids, + &mut buf1, + &mut buf2, + pack_cache, + &mut progress, + should_interrupt, + false, + #[cfg(feature = "object-cache-dynamic")] + object_cache_size_in_bytes, + ) +} + +mod expand { + use std::sync::atomic::{AtomicBool, Ordering}; + + use git_features::progress::Progress; + use git_hash::{oid, ObjectId}; + use git_object::{CommitRefIter, TagRefIter}; + + use super::{ + tree, + types::{Error, ObjectExpansion, Outcome}, + util, + }; + use crate::{ + cache::Object, + data::{output, output::count::PackLocation}, + find, FindExt, + }; + + #[allow(clippy::too_many_arguments)] + pub fn this( + db: &Find, + input_object_expansion: ObjectExpansion, + seen_objs: &impl util::InsertImmutable, + oids: impl IntoIterator>, + buf1: &mut Vec, + buf2: &mut Vec, + cache: &mut impl crate::cache::DecodeEntry, + progress: &mut impl Progress, + should_interrupt: &AtomicBool, + allow_pack_lookups: bool, + #[cfg(feature = "object-cache-dynamic")] object_cache_size_in_bytes: usize, + ) -> super::Result, IterErr> + where + Find: crate::Find + Send + Sync, + Oid: Into + Send, + IterErr: std::error::Error + Send, + { + use ObjectExpansion::*; + + let mut out = Vec::new(); + let mut tree_traversal_state = git_traverse::tree::breadthfirst::State::default(); + let mut tree_diff_state = git_diff::tree::State::default(); + let mut parent_commit_ids = Vec::new(); + let mut traverse_delegate = tree::traverse::AllUnseen::new(seen_objs); + let mut changes_delegate = tree::changes::AllNew::new(seen_objs); + let mut outcome = Outcome::default(); + #[cfg(feature = "object-cache-dynamic")] + let mut obj_cache = crate::cache::object::MemoryCappedHashmap::new(object_cache_size_in_bytes); + #[cfg(not(feature = "object-cache-dynamic"))] + let mut obj_cache = crate::cache::object::Never; + + let stats = &mut outcome; + for id in oids.into_iter() { + if should_interrupt.load(Ordering::Relaxed) { + return Err(Error::Interrupted); + } + + let id = id.map(|oid| oid.into()).map_err(Error::InputIteration)?; + let obj = db.find(id, buf1, cache)?; + stats.input_objects += 1; + match input_object_expansion { + TreeAdditionsComparedToAncestor => { + use git_object::Kind::*; + let mut obj = obj; + let mut id = id.to_owned(); + + loop { + push_obj_count_unique(&mut out, seen_objs, &id, &obj, progress, stats, false); + match obj.kind { + Tree | Blob => break, + Tag => { + id = TagRefIter::from_bytes(obj.data) + .target_id() + .expect("every tag has a target"); + obj = db.find(id, buf1, cache)?; + stats.expanded_objects += 1; + continue; + } + Commit => { + let current_tree_iter = { + let mut commit_iter = CommitRefIter::from_bytes(obj.data); + let tree_id = commit_iter.tree_id().expect("every commit has a tree"); + parent_commit_ids.clear(); + for token in commit_iter { + match token { + Ok(git_object::commit::ref_iter::Token::Parent { id }) => { + parent_commit_ids.push(id) + } + Ok(_) => break, + Err(err) => return Err(Error::CommitDecode(err)), + } + } + let obj = db.find(tree_id, buf1, cache)?; + push_obj_count_unique(&mut out, seen_objs, &tree_id, &obj, progress, stats, true); + git_object::TreeRefIter::from_bytes(obj.data) + }; + + let objects = if parent_commit_ids.is_empty() { + traverse_delegate.clear(); + git_traverse::tree::breadthfirst( + current_tree_iter, + &mut tree_traversal_state, + |oid, buf| { + stats.decoded_objects += 1; + match db.find(oid, buf, cache).ok() { + Some(obj) => { + progress.inc(); + stats.expanded_objects += 1; + out.push(output::Count::from_data(oid, &obj)); + obj.try_into_tree_iter() + } + None => None, + } + }, + &mut traverse_delegate, + ) + .map_err(Error::TreeTraverse)?; + &traverse_delegate.non_trees + } else { + for commit_id in &parent_commit_ids { + let parent_tree_id = { + let parent_commit_obj = db.find(commit_id, buf2, cache)?; + + push_obj_count_unique( + &mut out, + seen_objs, + commit_id, + &parent_commit_obj, + progress, + stats, + true, + ); + CommitRefIter::from_bytes(parent_commit_obj.data) + .tree_id() + .expect("every commit has a tree") + }; + let parent_tree = { + let parent_tree_obj = db.find(parent_tree_id, buf2, cache)?; + push_obj_count_unique( + &mut out, + seen_objs, + &parent_tree_id, + &parent_tree_obj, + progress, + stats, + true, + ); + git_object::TreeRefIter::from_bytes(parent_tree_obj.data) + }; + + changes_delegate.clear(); + git_diff::tree::Changes::from(Some(parent_tree)) + .needed_to_obtain( + current_tree_iter.clone(), + &mut tree_diff_state, + |oid, buf| { + stats.decoded_objects += 1; + let id = oid.to_owned(); + match obj_cache.get(&id, buf) { + Some(_kind) => git_object::TreeRefIter::from_bytes(buf).into(), + None => match db.find_tree_iter(oid, buf, cache).ok() { + Some(_) => { + obj_cache.put(id, git_object::Kind::Tree, buf); + git_object::TreeRefIter::from_bytes(buf).into() + } + None => None, + }, + } + }, + &mut changes_delegate, + ) + .map_err(Error::TreeChanges)?; + } + &changes_delegate.objects + }; + for id in objects.iter() { + out.push(id_to_count(db, buf2, id, progress, stats, allow_pack_lookups)); + } + break; + } + } + } + } + TreeContents => { + use git_object::Kind::*; + let mut id = id; + let mut obj = obj; + loop { + push_obj_count_unique(&mut out, seen_objs, &id, &obj, progress, stats, false); + match obj.kind { + Tree => { + traverse_delegate.clear(); + git_traverse::tree::breadthfirst( + git_object::TreeRefIter::from_bytes(obj.data), + &mut tree_traversal_state, + |oid, buf| { + stats.decoded_objects += 1; + match db.find(oid, buf, cache).ok() { + Some(obj) => { + progress.inc(); + stats.expanded_objects += 1; + out.push(output::Count::from_data(oid, &obj)); + obj.try_into_tree_iter() + } + None => None, + } + }, + &mut traverse_delegate, + ) + .map_err(Error::TreeTraverse)?; + for id in traverse_delegate.non_trees.iter() { + out.push(id_to_count(db, buf1, id, progress, stats, allow_pack_lookups)); + } + break; + } + Commit => { + id = CommitRefIter::from_bytes(obj.data) + .tree_id() + .expect("every commit has a tree"); + stats.expanded_objects += 1; + obj = db.find(id, buf1, cache)?; + continue; + } + Blob => break, + Tag => { + id = TagRefIter::from_bytes(obj.data) + .target_id() + .expect("every tag has a target"); + stats.expanded_objects += 1; + obj = db.find(id, buf1, cache)?; + continue; + } + } + } + } + AsIs => push_obj_count_unique(&mut out, seen_objs, &id, &obj, progress, stats, false), + } + } + Ok((out, outcome)) + } + + #[inline] + fn push_obj_count_unique( + out: &mut Vec, + all_seen: &impl util::InsertImmutable, + id: &oid, + obj: &crate::data::Object<'_>, + progress: &mut impl Progress, + statistics: &mut Outcome, + count_expanded: bool, + ) { + let inserted = all_seen.insert(id.to_owned()); + if inserted { + progress.inc(); + statistics.decoded_objects += 1; + if count_expanded { + statistics.expanded_objects += 1; + } + out.push(output::Count::from_data(id, obj)); + } + } + + #[inline] + fn id_to_count( + db: &Find, + buf: &mut Vec, + id: &oid, + progress: &mut impl Progress, + statistics: &mut Outcome, + allow_pack_lookups: bool, + ) -> output::Count { + progress.inc(); + statistics.expanded_objects += 1; + output::Count { + id: id.to_owned(), + entry_pack_location: if allow_pack_lookups { + PackLocation::LookedUp(db.location_by_oid(id, buf)) + } else { + PackLocation::NotLookedUp + }, + } + } +} diff --git a/git-pack/src/data/output/count/objects/reduce.rs b/git-pack/src/data/output/count/objects/reduce.rs new file mode 100644 index 00000000000..d5c76100559 --- /dev/null +++ b/git-pack/src/data/output/count/objects/reduce.rs @@ -0,0 +1,50 @@ +use std::{marker::PhantomData, sync::Arc}; + +use git_features::{parallel, progress::Progress}; + +use super::Outcome; +use crate::data::output; + +pub struct Statistics { + total: Outcome, + counts: Vec, + progress: Arc>, + _err: PhantomData, +} + +impl Statistics +where + P: Progress, +{ + pub fn new(progress: Arc>) -> Self { + Statistics { + total: Default::default(), + counts: Default::default(), + progress, + _err: PhantomData::default(), + } + } +} + +impl parallel::Reduce for Statistics +where + P: Progress, +{ + type Input = Result<(Vec, Outcome), E>; + type FeedProduce = (); + type Output = (Vec, Outcome); + type Error = E; + + fn feed(&mut self, item: Self::Input) -> Result { + let (counts, mut stats) = item?; + stats.total_objects = counts.len(); + self.total.aggregate(stats); + self.progress.lock().inc_by(counts.len()); + self.counts.extend(counts); + Ok(()) + } + + fn finalize(self) -> Result { + Ok((self.counts, self.total)) + } +} diff --git a/git-pack/src/data/output/count/objects/tree.rs b/git-pack/src/data/output/count/objects/tree.rs new file mode 100644 index 00000000000..7026fb209e2 --- /dev/null +++ b/git-pack/src/data/output/count/objects/tree.rs @@ -0,0 +1,114 @@ +pub mod changes { + use git_diff::tree::{ + visit::{Action, Change}, + Visit, + }; + use git_hash::ObjectId; + use git_object::bstr::BStr; + + use crate::data::output::count::objects_impl::util::InsertImmutable; + + pub struct AllNew<'a, H> { + pub objects: Vec, + all_seen: &'a H, + } + + impl<'a, H> AllNew<'a, H> + where + H: InsertImmutable, + { + pub fn new(all_seen: &'a H) -> Self { + AllNew { + objects: Default::default(), + all_seen, + } + } + pub fn clear(&mut self) { + self.objects.clear(); + } + } + + impl<'a, H> Visit for AllNew<'a, H> + where + H: InsertImmutable, + { + fn pop_front_tracked_path_and_set_current(&mut self) {} + + fn push_back_tracked_path_component(&mut self, _component: &BStr) {} + + fn push_path_component(&mut self, _component: &BStr) {} + + fn pop_path_component(&mut self) {} + + fn visit(&mut self, change: Change) -> Action { + match change { + Change::Addition { oid, .. } | Change::Modification { oid, .. } => { + let inserted = self.all_seen.insert(oid); + if inserted { + self.objects.push(oid); + } + } + Change::Deletion { .. } => {} + }; + Action::Continue + } + } +} + +pub mod traverse { + use git_hash::ObjectId; + use git_object::{bstr::BStr, tree::EntryRef}; + use git_traverse::tree::{visit::Action, Visit}; + + use crate::data::output::count::objects_impl::util::InsertImmutable; + + pub struct AllUnseen<'a, H> { + pub non_trees: Vec, + all_seen: &'a H, + } + + impl<'a, H> AllUnseen<'a, H> + where + H: InsertImmutable, + { + pub fn new(all_seen: &'a H) -> Self { + AllUnseen { + non_trees: Default::default(), + all_seen, + } + } + pub fn clear(&mut self) { + self.non_trees.clear(); + } + } + + impl<'a, H> Visit for AllUnseen<'a, H> + where + H: InsertImmutable, + { + fn pop_front_tracked_path_and_set_current(&mut self) {} + + fn push_back_tracked_path_component(&mut self, _component: &BStr) {} + + fn push_path_component(&mut self, _component: &BStr) {} + + fn pop_path_component(&mut self) {} + + fn visit_tree(&mut self, entry: &EntryRef<'_>) -> Action { + let inserted = self.all_seen.insert(entry.oid.to_owned()); + if inserted { + Action::Continue + } else { + Action::Skip + } + } + + fn visit_nontree(&mut self, entry: &EntryRef<'_>) -> Action { + let inserted = self.all_seen.insert(entry.oid.to_owned()); + if inserted { + self.non_trees.push(entry.oid.to_owned()); + } + Action::Continue + } + } +} diff --git a/git-pack/src/data/output/count/objects/types.rs b/git-pack/src/data/output/count/objects/types.rs new file mode 100644 index 00000000000..d9e2404a7d2 --- /dev/null +++ b/git-pack/src/data/output/count/objects/types.rs @@ -0,0 +1,115 @@ +/// Information gathered during the run of [`iter_from_objects()`][super::objects()]. +#[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +pub struct Outcome { + /// The amount of objects provided to start the iteration. + pub input_objects: usize, + /// The amount of objects that have been expanded from the input source. + /// It's desirable to do that as expansion happens on multiple threads, allowing the amount of input objects to be small. + /// `expanded_objects - decoded_objects` is the 'cheap' object we found without decoding the object itself. + pub expanded_objects: usize, + /// The amount of fully decoded objects. These are the most expensive as they are fully decoded + pub decoded_objects: usize, + /// The total amount of encountered objects. Should be `expanded_objects + input_objects`. + pub total_objects: usize, +} + +impl Outcome { + pub(in crate::data::output::count) fn aggregate( + &mut self, + Outcome { + input_objects, + decoded_objects, + expanded_objects, + total_objects, + }: Self, + ) { + self.input_objects += input_objects; + self.decoded_objects += decoded_objects; + self.expanded_objects += expanded_objects; + self.total_objects += total_objects; + } +} + +/// The way input objects are handled +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +pub enum ObjectExpansion { + /// Don't do anything with the input objects except for transforming them into pack entries + AsIs, + /// If the input object is a Commit then turn it into a pack entry. Additionally obtain its tree, turn it into a pack entry + /// along with all of its contents, that is nested trees, and any other objects reachable from it. + /// Otherwise, the same as [`AsIs`][ObjectExpansion::AsIs]. + /// + /// This mode is useful if all reachable objects should be added, as in cloning a repository. + TreeContents, + /// If the input is a commit, obtain its ancestors and turn them into pack entries. Obtain the ancestor trees along with the commits + /// tree and turn them into pack entries. Finally obtain the added/changed objects when comparing the ancestor trees with the + /// current tree and turn them into entries as well. + /// Otherwise, the same as [`AsIs`][ObjectExpansion::AsIs]. + /// + /// This mode is useful to build a pack containing only new objects compared to a previous state. + TreeAdditionsComparedToAncestor, +} + +impl Default for ObjectExpansion { + fn default() -> Self { + ObjectExpansion::AsIs + } +} + +/// Configuration options for the pack generation functions provied in [this module][crate::data::output]. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +pub struct Options { + /// The amount of threads to use at most when resolving the pack. If `None`, all logical cores are used. + /// If more than one thread is used, the order of returned [counts][crate::data::output::Count] is not deterministic anymore + /// especially when tree traversal is involved. Thus deterministic ordering requires `Some(1)` to be set. + pub thread_limit: Option, + /// The amount of objects per chunk or unit of work to be sent to threads for processing + pub chunk_size: usize, + /// The way input objects are handled + pub input_object_expansion: ObjectExpansion, + /// The size of a per-thread object cache in bytes to accelerate tree diffs in conjunction + /// with [ObjectExpansion::TreeAdditionsComparedToAncestor]. + /// + /// If zero, the cache is disabled but in a costly way. Consider using a low value instead. + /// + /// Defaults to 10 megabytes which usually leads to 2.5x speedups. + #[cfg(feature = "object-cache-dynamic")] + pub object_cache_size_in_bytes: usize, +} + +impl Default for Options { + fn default() -> Self { + Options { + thread_limit: None, + chunk_size: 10, + input_object_expansion: Default::default(), + #[cfg(feature = "object-cache-dynamic")] + object_cache_size_in_bytes: 10 * 1024 * 1024, + } + } +} + +/// The error returned by the pack generation iterator [bytes::FromEntriesIter][crate::data::output::bytes::FromEntriesIter]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error +where + FindErr: std::error::Error + 'static, + IterErr: std::error::Error + 'static, +{ + #[error(transparent)] + CommitDecode(git_object::decode::Error), + #[error(transparent)] + FindExisting(#[from] FindErr), + #[error(transparent)] + InputIteration(IterErr), + #[error(transparent)] + TreeTraverse(git_traverse::tree::breadthfirst::Error), + #[error(transparent)] + TreeChanges(git_diff::tree::changes::Error), + #[error("Operation interrupted")] + Interrupted, +} diff --git a/git-pack/src/data/output/count/objects/util.rs b/git-pack/src/data/output/count/objects/util.rs new file mode 100644 index 00000000000..fe1e282256f --- /dev/null +++ b/git-pack/src/data/output/count/objects/util.rs @@ -0,0 +1,48 @@ +pub trait InsertImmutable { + fn insert(&self, item: Item) -> bool; +} + +mod trait_impls { + use std::{cell::RefCell, collections::HashSet, hash::Hash}; + + use dashmap::DashSet; + + use super::InsertImmutable; + + impl InsertImmutable for DashSet { + fn insert(&self, item: T) -> bool { + self.insert(item) + } + } + + impl InsertImmutable for RefCell> { + fn insert(&self, item: T) -> bool { + self.borrow_mut().insert(item) + } + } +} + +pub struct Chunks { + pub size: usize, + pub iter: I, +} + +impl Iterator for Chunks +where + I: Iterator, +{ + type Item = Vec; + + fn next(&mut self) -> Option { + let mut res = Vec::with_capacity(self.size); + let mut items_left = self.size; + for item in &mut self.iter { + res.push(item); + items_left -= 1; + if items_left == 0 { + break; + } + } + (!res.is_empty()).then(|| res) + } +} diff --git a/git-repository/src/easy/ext/object.rs b/git-repository/src/easy/ext/object.rs index d06a267a230..89b926af739 100644 --- a/git-repository/src/easy/ext/object.rs +++ b/git-repository/src/easy/ext/object.rs @@ -2,6 +2,7 @@ use std::{convert::TryInto, ops::DerefMut}; use git_hash::ObjectId; use git_odb::{Find, FindExt}; +use git_pack::cache::Object; use git_ref::{ transaction::{LogChange, PreviousValue, RefLog}, FullName, @@ -12,7 +13,6 @@ use crate::{ easy::{commit, object, ObjectRef, Oid}, ext::ObjectIdExt, }; -use git_pack::cache::Object; /// Methods related to object creation. pub trait ObjectAccessExt: easy::Access + Sized { diff --git a/git-repository/src/easy/oid.rs b/git-repository/src/easy/oid.rs index e3b1f81cdc1..fa91e88b496 100644 --- a/git-repository/src/easy/oid.rs +++ b/git-repository/src/easy/oid.rs @@ -177,9 +177,9 @@ pub mod ancestors { } } pub use error::Error; + use git_pack::cache::Object; use crate::ext::ObjectIdExt; - use git_pack::cache::Object; } mod impls { diff --git a/gitoxide-core/src/pack/create.rs b/gitoxide-core/src/pack/create.rs index 14c206dd356..e25e2d24a1a 100644 --- a/gitoxide-core/src/pack/create.rs +++ b/gitoxide-core/src/pack/create.rs @@ -7,13 +7,12 @@ use git_repository::{ hash::ObjectId, interrupt, objs::bstr::ByteVec, - odb::{pack, pack::cache::DecodeEntry}, + odb::{pack, pack::cache::DecodeEntry, FindExt}, prelude::{Finalize, ReferenceAccessExt}, progress, traverse, Progress, }; use crate::OutputFormat; -use git_repository::odb::FindExt; pub const PROGRESS_RANGE: std::ops::RangeInclusive = 1..=2;