userland/git: git-compatible CLI on v2's gix code (T10)
Local workflow, smart-HTTP clone/fetch/push and public HTTPS clone/fetch match native git in stdout and exit codes. Only WASI imports (wasi:http, exit-with-code).
This commit is contained in:
+74
@@ -0,0 +1,74 @@
|
||||
use gix_error::Result;
|
||||
|
||||
impl crate::Bundle {
|
||||
/// Find an object with the given [`ObjectId`](gix_hash::ObjectId) and place its data into `out`.
|
||||
/// `inflate` is used to decompress objects, and will be reset before first use, but not after the last use.
|
||||
///
|
||||
/// [`cache`](crate::cache::DecodeEntry) is used to accelerate the lookup.
|
||||
///
|
||||
/// **Note** that ref deltas are automatically resolved within this pack only, which makes this implementation unusable
|
||||
/// for thin packs, which by now are expected to be resolved already.
|
||||
pub fn find<'a>(
|
||||
&self,
|
||||
id: &gix_hash::oid,
|
||||
out: &'a mut Vec<u8>,
|
||||
inflate: &mut gix_zlib::Inflate,
|
||||
cache: &mut dyn crate::cache::DecodeEntry,
|
||||
) -> Result<Option<(gix_object::Data<'a>, crate::data::entry::Location)>> {
|
||||
let idx = match self.index.lookup(id) {
|
||||
Some(idx) => idx,
|
||||
None => return Ok(None),
|
||||
};
|
||||
self.get_object_by_index(idx, out, inflate, cache).map(Some)
|
||||
}
|
||||
|
||||
/// Special-use function to get an object given an index previously returned from
|
||||
/// [index::File::](crate::index::File::lookup()).
|
||||
/// `inflate` is used to decompress objects, and will be reset before first use, but not after the last use.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If `index` is out of bounds.
|
||||
pub fn get_object_by_index<'a>(
|
||||
&self,
|
||||
idx: u32,
|
||||
out: &'a mut Vec<u8>,
|
||||
inflate: &mut gix_zlib::Inflate,
|
||||
cache: &mut dyn crate::cache::DecodeEntry,
|
||||
) -> Result<(gix_object::Data<'a>, crate::data::entry::Location)> {
|
||||
let ofs = self.index.pack_offset_at_index(idx);
|
||||
let pack_entry = self.pack.entry(ofs)?;
|
||||
let header_size = pack_entry.header_size();
|
||||
self.pack
|
||||
.decode_entry(
|
||||
pack_entry,
|
||||
out,
|
||||
inflate,
|
||||
&|id, _out| {
|
||||
self.index
|
||||
.lookup(id)
|
||||
.map(|idx| {
|
||||
self.pack
|
||||
.entry(self.index.pack_offset_at_index(idx))
|
||||
.map(crate::data::decode::entry::ResolvedBase::InPack)
|
||||
})
|
||||
.transpose()
|
||||
},
|
||||
cache,
|
||||
)
|
||||
.map(move |r| {
|
||||
(
|
||||
gix_object::Data {
|
||||
kind: r.kind,
|
||||
data: out.as_slice(),
|
||||
object_hash: self.pack.object_hash(),
|
||||
},
|
||||
crate::data::entry::Location {
|
||||
pack_id: self.pack.id,
|
||||
pack_offset: ofs,
|
||||
entry_size: r.compressed_size + header_size,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
use gix_error::{Result, validation};
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use gix_error::{OptionExt, bail};
|
||||
|
||||
use crate::Bundle;
|
||||
|
||||
/// Initialization
|
||||
impl Bundle {
|
||||
/// Create a `Bundle` from `path`, which is either a pack file _(*.pack)_ or an index file _(*.idx)_.
|
||||
///
|
||||
/// The corresponding complementary file is expected to be present.
|
||||
///
|
||||
/// The `object_hash` is a way to read (and write) the same file format with different hashes, as the hash kind
|
||||
/// isn't stored within the file format itself.
|
||||
pub fn at(path: impl AsRef<Path>, object_hash: gix_hash::Kind) -> Result<Self> {
|
||||
Self::at_inner(path.as_ref(), object_hash)
|
||||
}
|
||||
|
||||
fn at_inner(path: &Path, object_hash: gix_hash::Kind) -> Result<Self> {
|
||||
let ext = path.extension().and_then(std::ffi::OsStr::to_str).ok_or_raise(|| {
|
||||
validation(format!(
|
||||
"An 'idx' extension is expected of an index file: \"{}\"",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
Ok(match ext {
|
||||
"idx" => Self {
|
||||
index: crate::index::File::at(path, object_hash)?,
|
||||
pack: crate::data::File::at(path.with_extension("pack"), object_hash)?,
|
||||
},
|
||||
"pack" => Self {
|
||||
pack: crate::data::File::at(path, object_hash)?,
|
||||
index: crate::index::File::at(path.with_extension("idx"), object_hash)?,
|
||||
},
|
||||
_ => {
|
||||
bail!(validation(format!(
|
||||
"An 'idx' extension is expected of an index file: \"{}\"",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
///
|
||||
pub mod init;
|
||||
|
||||
mod find;
|
||||
///
|
||||
#[cfg(all(not(feature = "wasm"), feature = "streaming-input"))]
|
||||
pub mod write;
|
||||
|
||||
///
|
||||
pub mod verify {
|
||||
use gix_error::Result;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use gix_utils::progress::DynNestedProgress;
|
||||
|
||||
///
|
||||
pub mod integrity {
|
||||
/// Returned by [`Bundle::verify_integrity()`][crate::Bundle::verify_integrity()].
|
||||
pub struct Outcome {
|
||||
/// The computed checksum of the index which matched the stored one.
|
||||
pub actual_index_checksum: gix_hash::ObjectId,
|
||||
/// The packs traversal outcome
|
||||
pub pack_traverse_outcome: crate::index::traverse::Statistics,
|
||||
}
|
||||
}
|
||||
|
||||
use crate::Bundle;
|
||||
|
||||
impl Bundle {
|
||||
/// Similar to [`crate::index::File::verify_integrity()`] but more convenient to call as the presence of the
|
||||
/// pack file is a given.
|
||||
pub fn verify_integrity<C, F>(
|
||||
&self,
|
||||
progress: &mut dyn DynNestedProgress,
|
||||
should_interrupt: &AtomicBool,
|
||||
options: crate::index::verify::integrity::Options<F>,
|
||||
) -> Result<integrity::Outcome>
|
||||
where
|
||||
C: crate::cache::DecodeEntry,
|
||||
F: Fn() -> C + Send + Clone,
|
||||
{
|
||||
self.index
|
||||
.verify_integrity(
|
||||
Some(crate::index::verify::PackContext {
|
||||
data: &self.pack,
|
||||
options,
|
||||
}),
|
||||
progress,
|
||||
should_interrupt,
|
||||
)
|
||||
.map(|o| integrity::Outcome {
|
||||
actual_index_checksum: o.actual_index_checksum,
|
||||
pack_traverse_outcome: o.pack_traverse_statistics.expect("pack is set"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
use gix_error::Result;
|
||||
use std::{
|
||||
io,
|
||||
io::Write,
|
||||
marker::PhantomData,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
|
||||
use gix_error::{ResultExt, message};
|
||||
use gix_tempfile::{AutoRemove, ContainingDirectory};
|
||||
use gix_utils::interrupt;
|
||||
use gix_utils::progress;
|
||||
use gix_utils::progress::Progress;
|
||||
|
||||
use crate::data;
|
||||
|
||||
use gix_utils::progress::prodash::DynNestedProgress;
|
||||
|
||||
mod types;
|
||||
use types::{LockWriter, PassThrough};
|
||||
pub use types::{Options, Outcome};
|
||||
|
||||
use crate::bundle::write::types::SharedTempFile;
|
||||
|
||||
/// The progress ids used in [`write_to_directory()`][crate::Bundle::write_to_directory()].
|
||||
///
|
||||
/// Use this information to selectively extract the progress of interest in case the parent application has custom visualization.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum ProgressId {
|
||||
/// The amount of bytes read from the input pack data file.
|
||||
ReadPackBytes,
|
||||
/// A root progress counting logical steps towards an index file on disk.
|
||||
///
|
||||
/// Underneath will be more progress information related to actually producing the index.
|
||||
IndexingSteps(PhantomData<crate::index::write::ProgressId>),
|
||||
}
|
||||
|
||||
impl From<ProgressId> for gix_utils::progress::Id {
|
||||
fn from(v: ProgressId) -> Self {
|
||||
match v {
|
||||
ProgressId::ReadPackBytes => *b"BWRB",
|
||||
ProgressId::IndexingSteps(_) => *b"BWCI",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Bundle {
|
||||
/// Given a `pack` data stream, write it along with a generated index into the `directory` if `Some` or discard all output if `None`.
|
||||
///
|
||||
/// In the latter case, the functionality provided here is more a kind of pack data stream validation.
|
||||
///
|
||||
/// * `progress` provides detailed progress information which can be discarded with [`gix_utils::progress::Discard`].
|
||||
/// * `should_interrupt` is checked regularly and when true, the whole operation will stop.
|
||||
/// * `thin_pack_base_object_lookup` retrieves external bases to insert into the output pack, completing a thin pack.
|
||||
/// Ref-deltas whose bases are not found through this lookup are resolved in-pack during indexing, including forward
|
||||
/// references. A base available neither through the lookup nor in the pack causes the operation to fail.
|
||||
/// * `object_hash` specifies the hash to use for writing the bundle.
|
||||
/// * `options` further configure how the task is performed.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// * the resulting pack may be empty, that is, contains zero objects in some situations. This is a valid reply by a server and should
|
||||
/// be accounted for.
|
||||
/// - Empty packs always have the same name and not handling this case will result in at most one superfluous pack.
|
||||
pub fn write_to_directory(
|
||||
pack: &mut dyn io::BufRead,
|
||||
directory: Option<&Path>,
|
||||
progress: &mut dyn DynNestedProgress,
|
||||
should_interrupt: &AtomicBool,
|
||||
thin_pack_base_object_lookup: Option<impl gix_object::Find>,
|
||||
object_hash: gix_hash::Kind,
|
||||
options: Options,
|
||||
) -> Result<Outcome> {
|
||||
let _span = gix_trace::coarse!("gix_pack::Bundle::write_to_directory()");
|
||||
let mut read_progress = progress.add_child_with_id("read pack".into(), ProgressId::ReadPackBytes.into());
|
||||
read_progress.init(None, progress::bytes());
|
||||
let pack = progress::Read {
|
||||
inner: pack,
|
||||
progress: progress::ThroughputOnDrop::new(read_progress),
|
||||
};
|
||||
|
||||
let data_file = Arc::new(parking_lot::Mutex::new(io::BufWriter::with_capacity(
|
||||
64 * 1024,
|
||||
match directory.as_ref() {
|
||||
Some(directory) => gix_tempfile::new(directory, ContainingDirectory::Exists, AutoRemove::Tempfile)
|
||||
.or_raise(|| message("Could not create temporary pack file"))?,
|
||||
None => gix_tempfile::new(std::env::temp_dir(), ContainingDirectory::Exists, AutoRemove::Tempfile)
|
||||
.or_raise(|| message("Could not create temporary pack file"))?,
|
||||
},
|
||||
)));
|
||||
let (pack_entries_iter, pack_version): (Box<dyn Iterator<Item = Result<data::input::Entry>>>, _) =
|
||||
match thin_pack_base_object_lookup {
|
||||
Some(thin_pack_lookup) => {
|
||||
let pack = interrupt::Read {
|
||||
inner: pack,
|
||||
should_interrupt,
|
||||
};
|
||||
let buffered_pack = io::BufReader::new(pack);
|
||||
let pack_entries_iter = data::input::LookupRefDeltaObjectsIter::new(
|
||||
data::input::BytesToEntriesIter::new_from_header(
|
||||
buffered_pack,
|
||||
options.iteration_mode,
|
||||
data::input::EntryDataMode::KeepAndCrc32,
|
||||
object_hash,
|
||||
)?,
|
||||
thin_pack_lookup,
|
||||
options.compression,
|
||||
);
|
||||
let pack_version = pack_entries_iter.inner.version();
|
||||
let pack_entries_iter = data::input::EntriesToBytesIter::new(
|
||||
pack_entries_iter,
|
||||
LockWriter {
|
||||
writer: data_file.clone(),
|
||||
},
|
||||
pack_version,
|
||||
object_hash,
|
||||
);
|
||||
(Box::new(pack_entries_iter), pack_version)
|
||||
}
|
||||
None => {
|
||||
let pack = PassThrough {
|
||||
reader: interrupt::Read {
|
||||
inner: pack,
|
||||
should_interrupt,
|
||||
},
|
||||
writer: Some(data_file.clone()),
|
||||
};
|
||||
// This buf-reader is required to assure we call 'read()' in order to fill the (extra) buffer. Otherwise all the counting
|
||||
// we do with the wrapped pack reader doesn't work as it does not expect anyone to call BufRead functions directly.
|
||||
// However, this is exactly what's happening in the ZipReader implementation that is eventually used.
|
||||
// The performance impact of this is probably negligible, compared to all the other work that is done anyway :D.
|
||||
let buffered_pack = io::BufReader::new(pack);
|
||||
let pack_entries_iter = data::input::BytesToEntriesIter::new_from_header(
|
||||
buffered_pack,
|
||||
options.iteration_mode,
|
||||
data::input::EntryDataMode::Crc32,
|
||||
object_hash,
|
||||
)?;
|
||||
let pack_version = pack_entries_iter.version();
|
||||
(Box::new(pack_entries_iter), pack_version)
|
||||
}
|
||||
};
|
||||
let WriteOutcome {
|
||||
outcome,
|
||||
data_path,
|
||||
index_path,
|
||||
keep_path,
|
||||
} = crate::Bundle::inner_write(
|
||||
directory,
|
||||
progress,
|
||||
object_hash,
|
||||
options,
|
||||
data_file,
|
||||
pack_entries_iter,
|
||||
should_interrupt,
|
||||
pack_version,
|
||||
)?;
|
||||
|
||||
Ok(Outcome {
|
||||
index: outcome,
|
||||
object_hash,
|
||||
pack_version,
|
||||
data_path,
|
||||
index_path,
|
||||
keep_path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Equivalent to [`write_to_directory()`][crate::Bundle::write_to_directory()] but offloads reading of the pack into its own thread, hence the `Send + 'static'` bounds.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// As it sends portions of the input to a thread it requires the 'static lifetime for the interrupt flags. This can only
|
||||
/// be satisfied by a static `AtomicBool` which is only suitable for programs that only run one of these operations at a time
|
||||
/// or don't mind that all of them abort when the flag is set.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub fn write_to_directory_eagerly(
|
||||
pack: Box<dyn io::Read + Send + 'static>,
|
||||
pack_size: Option<u64>,
|
||||
directory: Option<impl AsRef<Path>>,
|
||||
progress: &mut dyn DynNestedProgress,
|
||||
should_interrupt: &'static AtomicBool,
|
||||
thin_pack_base_object_lookup: Option<impl gix_object::Find + Send + 'static>,
|
||||
object_hash: gix_hash::Kind,
|
||||
options: Options,
|
||||
) -> Result<Outcome> {
|
||||
let _span = gix_trace::coarse!("gix_pack::Bundle::write_to_directory_eagerly()");
|
||||
let mut read_progress = progress.add_child_with_id("read pack".into(), ProgressId::ReadPackBytes.into()); /* Bundle Write Read pack Bytes*/
|
||||
read_progress.init(pack_size.map(|s| s as usize), progress::bytes());
|
||||
let pack = progress::Read {
|
||||
inner: pack,
|
||||
progress: progress::ThroughputOnDrop::new(read_progress),
|
||||
};
|
||||
|
||||
let data_file = Arc::new(parking_lot::Mutex::new(io::BufWriter::new(match directory.as_ref() {
|
||||
Some(directory) => gix_tempfile::new(directory, ContainingDirectory::Exists, AutoRemove::Tempfile)
|
||||
.or_raise(|| message("Could not create temporary pack file"))?,
|
||||
None => gix_tempfile::new(std::env::temp_dir(), ContainingDirectory::Exists, AutoRemove::Tempfile)
|
||||
.or_raise(|| message("Could not create temporary pack file"))?,
|
||||
})));
|
||||
let eight_pages = 4096 * 8;
|
||||
let (pack_entries_iter, pack_version): (
|
||||
Box<dyn Iterator<Item = Result<data::input::Entry>> + Send + 'static>,
|
||||
_,
|
||||
) = match thin_pack_base_object_lookup {
|
||||
Some(thin_pack_lookup) => {
|
||||
let pack = interrupt::Read {
|
||||
inner: pack,
|
||||
should_interrupt,
|
||||
};
|
||||
let buffered_pack = io::BufReader::with_capacity(eight_pages, pack);
|
||||
let pack_entries_iter = data::input::LookupRefDeltaObjectsIter::new(
|
||||
data::input::BytesToEntriesIter::new_from_header(
|
||||
buffered_pack,
|
||||
options.iteration_mode,
|
||||
data::input::EntryDataMode::KeepAndCrc32,
|
||||
object_hash,
|
||||
)?,
|
||||
thin_pack_lookup,
|
||||
options.compression,
|
||||
);
|
||||
let pack_kind = pack_entries_iter.inner.version();
|
||||
(Box::new(pack_entries_iter), pack_kind)
|
||||
}
|
||||
None => {
|
||||
let pack = PassThrough {
|
||||
reader: interrupt::Read {
|
||||
inner: pack,
|
||||
should_interrupt,
|
||||
},
|
||||
writer: Some(data_file.clone()),
|
||||
};
|
||||
let buffered_pack = io::BufReader::with_capacity(eight_pages, pack);
|
||||
let pack_entries_iter = data::input::BytesToEntriesIter::new_from_header(
|
||||
buffered_pack,
|
||||
options.iteration_mode,
|
||||
data::input::EntryDataMode::Crc32,
|
||||
object_hash,
|
||||
)?;
|
||||
let pack_kind = pack_entries_iter.version();
|
||||
(Box::new(pack_entries_iter), pack_kind)
|
||||
}
|
||||
};
|
||||
let num_objects = pack_entries_iter.size_hint().0;
|
||||
let pack_entries_iter =
|
||||
gix_parallel::EagerIterIf::new(move || num_objects > 25_000, pack_entries_iter, 5_000, 5);
|
||||
|
||||
let WriteOutcome {
|
||||
outcome,
|
||||
data_path,
|
||||
index_path,
|
||||
keep_path,
|
||||
} = crate::Bundle::inner_write(
|
||||
directory,
|
||||
progress,
|
||||
object_hash,
|
||||
options,
|
||||
data_file,
|
||||
Box::new(pack_entries_iter),
|
||||
should_interrupt,
|
||||
pack_version,
|
||||
)?;
|
||||
|
||||
Ok(Outcome {
|
||||
index: outcome,
|
||||
object_hash,
|
||||
pack_version,
|
||||
data_path,
|
||||
index_path,
|
||||
keep_path,
|
||||
})
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn inner_write<'a>(
|
||||
directory: Option<impl AsRef<Path>>,
|
||||
progress: &mut dyn DynNestedProgress,
|
||||
object_hash: gix_hash::Kind,
|
||||
Options {
|
||||
thread_limit,
|
||||
iteration_mode: _,
|
||||
index_version: index_kind,
|
||||
alloc_limit_bytes,
|
||||
compression: _,
|
||||
}: Options,
|
||||
data_file: SharedTempFile,
|
||||
mut pack_entries_iter: Box<dyn Iterator<Item = Result<data::input::Entry>> + 'a>,
|
||||
should_interrupt: &AtomicBool,
|
||||
pack_version: data::Version,
|
||||
) -> Result<WriteOutcome> {
|
||||
let mut indexing_progress = progress.add_child_with_id(
|
||||
"create index file".into(),
|
||||
ProgressId::IndexingSteps(Default::default()).into(),
|
||||
);
|
||||
Ok(match directory {
|
||||
Some(directory) => {
|
||||
let directory = directory.as_ref();
|
||||
let mut index_file = gix_tempfile::new(directory, ContainingDirectory::Exists, AutoRemove::Tempfile)
|
||||
.or_raise(|| message("Could not create temporary index file"))?;
|
||||
|
||||
let outcome = crate::index::write_data_iter_to_stream(
|
||||
index_kind,
|
||||
{
|
||||
let data_file = Arc::clone(&data_file);
|
||||
move || new_pack_file_resolver(data_file)
|
||||
},
|
||||
&mut pack_entries_iter,
|
||||
thread_limit,
|
||||
&mut indexing_progress,
|
||||
&mut index_file,
|
||||
should_interrupt,
|
||||
object_hash,
|
||||
alloc_limit_bytes,
|
||||
pack_version,
|
||||
)?;
|
||||
drop(pack_entries_iter);
|
||||
|
||||
if outcome.num_objects == 0 {
|
||||
WriteOutcome {
|
||||
outcome,
|
||||
data_path: None,
|
||||
index_path: None,
|
||||
keep_path: None,
|
||||
}
|
||||
} else {
|
||||
let data_path = directory.join(format!("pack-{}.pack", outcome.data_hash.to_hex()));
|
||||
let index_path = data_path.with_extension("idx");
|
||||
let keep_path = if data_path.is_file() {
|
||||
// avoid trying to overwrite existing files, we know they have the same content
|
||||
// and this is likely to fail on Windows as negotiation opened the pack.
|
||||
None
|
||||
} else {
|
||||
let keep_path = data_path.with_extension("keep");
|
||||
|
||||
std::fs::write(&keep_path, b"").or_raise(|| message("Could not create pack keep file"))?;
|
||||
Arc::try_unwrap(data_file)
|
||||
.expect("only one handle left after pack was consumed")
|
||||
.into_inner()
|
||||
.into_inner()
|
||||
.or_error()?
|
||||
.persist(&data_path)
|
||||
.or_raise(|| message("Could not persist pack file"))?;
|
||||
Some(keep_path)
|
||||
};
|
||||
if !index_path.is_file() {
|
||||
index_file
|
||||
.persist(&index_path)
|
||||
.inspect_err(|_err| {
|
||||
gix_trace::warn!("pack file at \"{}\" is retained despite failing to move the index file into place. You can use plumbing to make it usable.",data_path.display());
|
||||
})
|
||||
.or_raise(|| message("Could not persist pack index"))?;
|
||||
}
|
||||
WriteOutcome {
|
||||
outcome,
|
||||
data_path: Some(data_path),
|
||||
index_path: Some(index_path),
|
||||
keep_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
None => WriteOutcome {
|
||||
outcome: crate::index::write_data_iter_to_stream(
|
||||
index_kind,
|
||||
move || new_pack_file_resolver(data_file),
|
||||
&mut pack_entries_iter,
|
||||
thread_limit,
|
||||
&mut indexing_progress,
|
||||
&mut io::sink(),
|
||||
should_interrupt,
|
||||
object_hash,
|
||||
alloc_limit_bytes,
|
||||
pack_version,
|
||||
)?,
|
||||
data_path: None,
|
||||
index_path: None,
|
||||
keep_path: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_entry(range: data::EntryRange, mapped_file: &crate::MMap) -> Option<&[u8]> {
|
||||
mapped_file.get(range.start as usize..range.end as usize)
|
||||
}
|
||||
|
||||
#[expect(clippy::type_complexity)] // cannot typedef impl Fn
|
||||
fn new_pack_file_resolver(
|
||||
data_file: SharedTempFile,
|
||||
) -> io::Result<(
|
||||
impl Fn(data::EntryRange, &crate::MMap) -> Option<&[u8]> + Send + Clone,
|
||||
crate::MMap,
|
||||
)> {
|
||||
let mut guard = data_file.lock();
|
||||
guard.flush()?;
|
||||
let mapped_file = crate::mmap::read_only(&guard.get_mut().with_mut(|f| f.path().to_owned())?)?;
|
||||
Ok((resolve_entry, mapped_file))
|
||||
}
|
||||
|
||||
struct WriteOutcome {
|
||||
outcome: crate::index::write::Outcome,
|
||||
data_path: Option<PathBuf>,
|
||||
index_path: Option<PathBuf>,
|
||||
keep_path: Option<PathBuf>,
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
use gix_error::Result;
|
||||
use std::{hash::Hash, io, io::SeekFrom, path::PathBuf, sync::Arc};
|
||||
|
||||
use gix_tempfile::handle::Writable;
|
||||
|
||||
/// Configuration for [`write_to_directory`][crate::Bundle::write_to_directory()] or
|
||||
/// [`write_to_directory_eagerly`][crate::Bundle::write_to_directory_eagerly()]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Options {
|
||||
/// The amount of threads to use at most when resolving the pack. If `None`, all logical cores are used.
|
||||
pub thread_limit: Option<usize>,
|
||||
/// Determine how much processing to spend on protecting against corruption or recovering from errors.
|
||||
pub iteration_mode: crate::data::input::Mode,
|
||||
/// The version of pack index to write, should be [`crate::index::Version::default()`]
|
||||
pub index_version: crate::index::Version,
|
||||
/// If `Some`, rejects individual allocations above the given number of bytes while building the delta tree or resolving
|
||||
/// streamed pack entries into decoded object and delta result buffers to write the index.
|
||||
/// `Some(0)` rejects all non-empty allocations.
|
||||
pub alloc_limit_bytes: Option<usize>,
|
||||
/// The compression level to use when deflating base objects that are added to the pack to complete a thin pack.
|
||||
///
|
||||
/// Defaults to [`Compression::BEST_SPEED`](gix_zlib::Compression::BEST_SPEED) - `git` compresses
|
||||
/// these with the same level it uses for loose objects, which is `core.looseCompression` or
|
||||
/// `core.compression`, or level 1 if neither is set.
|
||||
pub compression: gix_zlib::Compression,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
/// Options which favor speed and correctness and write the most commonly supported index file.
|
||||
fn default() -> Self {
|
||||
Options {
|
||||
thread_limit: None,
|
||||
iteration_mode: crate::data::input::Mode::Verify,
|
||||
index_version: Default::default(),
|
||||
alloc_limit_bytes: None,
|
||||
compression: gix_zlib::Compression::BEST_SPEED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returned by [`write_to_directory`][crate::Bundle::write_to_directory()] or
|
||||
/// [`write_to_directory_eagerly`][crate::Bundle::write_to_directory_eagerly()]
|
||||
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Outcome {
|
||||
/// The successful result of the index write operation.
|
||||
pub index: crate::index::write::Outcome,
|
||||
/// The version of the pack.
|
||||
pub pack_version: crate::data::Version,
|
||||
/// The kind of hash stored within the pack and indices.
|
||||
pub object_hash: gix_hash::Kind,
|
||||
|
||||
/// The path to the pack index file.
|
||||
pub index_path: Option<PathBuf>,
|
||||
/// The path to the pack data file.
|
||||
pub data_path: Option<PathBuf>,
|
||||
/// The path to the `.keep` file to prevent collection of the newly written pack until refs are pointing to it.
|
||||
/// It might be `None` if the file at `data_path` already existed, indicating that we have received a pack that
|
||||
/// was already present locally.
|
||||
///
|
||||
/// The file is created right before moving the pack data and index data into place (i.e. `data_path` and `index_path`)
|
||||
/// and is expected to be removed by the caller when ready.
|
||||
pub keep_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Outcome {
|
||||
/// Instantiate a bundle from the newly written index and data file that are represented by this `Outcome`
|
||||
pub fn to_bundle(&self) -> Option<Result<crate::Bundle>> {
|
||||
self.index_path
|
||||
.as_ref()
|
||||
.map(|path| crate::Bundle::at(path, self.object_hash))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type SharedTempFile = Arc<parking_lot::Mutex<std::io::BufWriter<gix_tempfile::Handle<Writable>>>>;
|
||||
|
||||
pub(crate) struct PassThrough<R> {
|
||||
pub reader: R,
|
||||
pub writer: Option<SharedTempFile>,
|
||||
}
|
||||
|
||||
impl<R> io::Read for PassThrough<R>
|
||||
where
|
||||
R: io::Read,
|
||||
{
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let bytes_read = self.reader.read(buf)?;
|
||||
if let Some(writer) = self.writer.as_mut() {
|
||||
use std::io::Write;
|
||||
writer.lock().write_all(&buf[..bytes_read])?;
|
||||
}
|
||||
Ok(bytes_read)
|
||||
}
|
||||
}
|
||||
impl<R> io::BufRead for PassThrough<R>
|
||||
where
|
||||
R: io::BufRead,
|
||||
{
|
||||
fn fill_buf(&mut self) -> io::Result<&[u8]> {
|
||||
self.reader.fill_buf()
|
||||
}
|
||||
|
||||
fn consume(&mut self, amt: usize) {
|
||||
self.reader.consume(amt);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct LockWriter {
|
||||
pub writer: SharedTempFile,
|
||||
}
|
||||
|
||||
impl io::Write for LockWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.writer.lock().write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.lock().flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl io::Read for LockWriter {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.writer.lock().get_mut().read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl io::Seek for LockWriter {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.writer.lock().seek(pos)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user