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).
230 lines
7.5 KiB
Rust
230 lines
7.5 KiB
Rust
use gix_error::Result;
|
|
use std::io::Write;
|
|
|
|
use crate::{State, Version, entry, extension, write::util::CountBytes};
|
|
|
|
/// A way to specify which of the optional extensions to write.
|
|
#[derive(Default, Debug, Copy, Clone)]
|
|
pub enum Extensions {
|
|
/// Writes all available optional extensions to avoid losing any information.
|
|
#[default]
|
|
All,
|
|
/// Only write the given optional extensions, with each extension being marked by a boolean flag.
|
|
///
|
|
/// # Note: mandatory extensions
|
|
///
|
|
/// Mandatory extensions, like `sdir` or other lower-case ones, may not be configured here as they need to be present
|
|
/// or absent depending on the state of the index itself and for it to be valid.
|
|
Given {
|
|
/// Write the tree-cache extension, if present.
|
|
tree_cache: bool,
|
|
/// Write the end-of-index-entry extension.
|
|
end_of_index_entry: bool,
|
|
},
|
|
/// Write no optional extension at all for what should be the smallest possible index
|
|
None,
|
|
}
|
|
|
|
impl Extensions {
|
|
/// Returns `Some(signature)` if it should be written out.
|
|
pub fn should_write(&self, signature: extension::Signature) -> Option<extension::Signature> {
|
|
match self {
|
|
Extensions::None => None,
|
|
Extensions::All => Some(signature),
|
|
Extensions::Given {
|
|
tree_cache,
|
|
end_of_index_entry,
|
|
} => match signature {
|
|
extension::tree::SIGNATURE => tree_cache,
|
|
extension::end_of_index_entry::SIGNATURE => end_of_index_entry,
|
|
_ => &false,
|
|
}
|
|
.then(|| signature),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The options for use when [writing an index][State::write_to()].
|
|
///
|
|
/// Note that default options write either index V2 or V3 depending on the content of the entries.
|
|
#[derive(Debug, Default, Clone, Copy)]
|
|
pub struct Options {
|
|
/// Configures which extensions to write.
|
|
pub extensions: Extensions,
|
|
/// Set the trailing hash of the produced index to all zeroes to save some time.
|
|
///
|
|
/// This value is typically controlled by `index.skipHash` and is respected when the index is written
|
|
/// via [`File::write()`](crate::File::write()) and [`File::write_to()`](crate::File::write_to()).
|
|
/// Note that
|
|
pub skip_hash: bool,
|
|
}
|
|
|
|
impl State {
|
|
/// Serialize this instance to `out` with [`options`][Options].
|
|
///
|
|
/// Note that the `tree` (tree-cache) extension is written as-is and is **not** recomputed or
|
|
/// invalidated to match the entries; see [`File::write()`](crate::File::write()) for the
|
|
/// implications and the recommended workaround.
|
|
pub fn write_to(
|
|
&self,
|
|
out: impl std::io::Write,
|
|
Options {
|
|
extensions,
|
|
skip_hash: _,
|
|
}: Options,
|
|
) -> Result<Version> {
|
|
let _span = gix_trace::detail!("gix_index::State::write()");
|
|
let version = self.detect_required_version();
|
|
|
|
let mut write = CountBytes::new(out);
|
|
let num_entries: u32 = self
|
|
.entries()
|
|
.len()
|
|
.try_into()
|
|
.expect("definitely not 4billion entries");
|
|
let removed_entries: u32 = self
|
|
.entries()
|
|
.iter()
|
|
.filter(|e| e.flags.contains(entry::Flags::REMOVE))
|
|
.count()
|
|
.try_into()
|
|
.expect("definitely not too many entries");
|
|
|
|
let offset_to_entries =
|
|
header(&mut write, version, num_entries - removed_entries).map_err(gix_hash::io::from_std_io)?;
|
|
let offset_to_extensions = entries(&mut write, self, offset_to_entries).map_err(gix_hash::io::from_std_io)?;
|
|
let (extension_toc, out) = self
|
|
.write_extensions(write, offset_to_extensions, extensions)
|
|
.map_err(gix_hash::io::from_std_io)?;
|
|
|
|
if num_entries > 0
|
|
&& extensions
|
|
.should_write(extension::end_of_index_entry::SIGNATURE)
|
|
.is_some()
|
|
&& !extension_toc.is_empty()
|
|
{
|
|
extension::end_of_index_entry::write_to(out, self.object_hash, offset_to_extensions, extension_toc)?;
|
|
}
|
|
|
|
Ok(version)
|
|
}
|
|
|
|
fn write_extensions<T>(
|
|
&self,
|
|
mut write: CountBytes<T>,
|
|
offset_to_extensions: u32,
|
|
extensions: Extensions,
|
|
) -> std::io::Result<(Vec<(extension::Signature, u32)>, T)>
|
|
where
|
|
T: std::io::Write,
|
|
{
|
|
type WriteExtFn<'a> = &'a dyn Fn(&mut dyn std::io::Write) -> Option<std::io::Result<extension::Signature>>;
|
|
let extensions: &[WriteExtFn<'_>] = &[
|
|
&|write| {
|
|
extensions
|
|
.should_write(extension::tree::SIGNATURE)
|
|
.and_then(|signature| self.tree().map(|tree| tree.write_to(write).map(|_| signature)))
|
|
},
|
|
&|write| {
|
|
self.is_sparse()
|
|
.then(|| extension::sparse::write_to(write).map(|_| extension::sparse::SIGNATURE))
|
|
},
|
|
];
|
|
|
|
let mut offset_to_previous_ext = offset_to_extensions;
|
|
let mut out = Vec::with_capacity(5);
|
|
for write_ext in extensions {
|
|
if let Some(signature) = write_ext(&mut write).transpose()? {
|
|
let offset_past_ext = write.count;
|
|
let ext_size = offset_past_ext - offset_to_previous_ext - (extension::MIN_SIZE as u32);
|
|
offset_to_previous_ext = offset_past_ext;
|
|
out.push((signature, ext_size));
|
|
}
|
|
}
|
|
Ok((out, write.inner))
|
|
}
|
|
}
|
|
|
|
impl State {
|
|
fn detect_required_version(&self) -> Version {
|
|
self.entries
|
|
.iter()
|
|
.find_map(|e| e.flags.contains(entry::Flags::EXTENDED).then_some(Version::V3))
|
|
.unwrap_or(Version::V2)
|
|
}
|
|
}
|
|
|
|
fn header<T: std::io::Write>(
|
|
out: &mut CountBytes<T>,
|
|
version: Version,
|
|
num_entries: u32,
|
|
) -> std::result::Result<u32, std::io::Error> {
|
|
let version = match version {
|
|
Version::V2 => 2_u32.to_be_bytes(),
|
|
Version::V3 => 3_u32.to_be_bytes(),
|
|
Version::V4 => 4_u32.to_be_bytes(),
|
|
};
|
|
|
|
out.write_all(crate::decode::header::SIGNATURE)?;
|
|
out.write_all(&version)?;
|
|
out.write_all(&num_entries.to_be_bytes())?;
|
|
|
|
Ok(out.count)
|
|
}
|
|
|
|
fn entries<T: std::io::Write>(
|
|
out: &mut CountBytes<T>,
|
|
state: &State,
|
|
header_size: u32,
|
|
) -> std::result::Result<u32, std::io::Error> {
|
|
for entry in state.entries() {
|
|
if entry.flags.contains(entry::Flags::REMOVE) {
|
|
continue;
|
|
}
|
|
entry.write_to(&mut *out, state)?;
|
|
match (out.count - header_size) % 8 {
|
|
0 => {}
|
|
n => {
|
|
let eight_null_bytes = [0u8; 8];
|
|
out.write_all(&eight_null_bytes[n as usize..])?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(out.count)
|
|
}
|
|
|
|
mod util {
|
|
pub struct CountBytes<T> {
|
|
pub count: u32,
|
|
pub inner: T,
|
|
}
|
|
|
|
impl<T> CountBytes<T>
|
|
where
|
|
T: std::io::Write,
|
|
{
|
|
pub fn new(inner: T) -> Self {
|
|
CountBytes { inner, count: 0 }
|
|
}
|
|
}
|
|
|
|
impl<T> std::io::Write for CountBytes<T>
|
|
where
|
|
T: std::io::Write,
|
|
{
|
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
|
let written = self.inner.write(buf)?;
|
|
self.count = self
|
|
.count
|
|
.checked_add(u32::try_from(written).expect("we don't write 4GB buffers"))
|
|
.ok_or_else(|| std::io::Error::other("Cannot write indices larger than 4 gigabytes"))?;
|
|
Ok(written)
|
|
}
|
|
|
|
fn flush(&mut self) -> std::io::Result<()> {
|
|
self.inner.flush()
|
|
}
|
|
}
|
|
}
|