diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
commit | 10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 (patch) | |
tree | bdffd5d80c26cf4a7a518281a204be1ace85b4c1 /vendor/gix-ref/src/store/packed | |
parent | Releasing progress-linux version 1.70.0+dfsg1-9~progress7.99u1. (diff) | |
download | rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.tar.xz rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.zip |
Merging upstream version 1.70.0+dfsg2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/gix-ref/src/store/packed')
-rw-r--r-- | vendor/gix-ref/src/store/packed/buffer.rs | 105 | ||||
-rw-r--r-- | vendor/gix-ref/src/store/packed/decode.rs | 83 | ||||
-rw-r--r-- | vendor/gix-ref/src/store/packed/decode/tests.rs | 125 | ||||
-rw-r--r-- | vendor/gix-ref/src/store/packed/find.rs | 154 | ||||
-rw-r--r-- | vendor/gix-ref/src/store/packed/iter.rs | 117 | ||||
-rw-r--r-- | vendor/gix-ref/src/store/packed/mod.rs | 93 | ||||
-rw-r--r-- | vendor/gix-ref/src/store/packed/transaction.rs | 267 |
7 files changed, 944 insertions, 0 deletions
diff --git a/vendor/gix-ref/src/store/packed/buffer.rs b/vendor/gix-ref/src/store/packed/buffer.rs new file mode 100644 index 000000000..6786e4a9f --- /dev/null +++ b/vendor/gix-ref/src/store/packed/buffer.rs @@ -0,0 +1,105 @@ +use crate::store_impl::packed; + +impl AsRef<[u8]> for packed::Buffer { + fn as_ref(&self) -> &[u8] { + &self.data.as_ref()[self.offset..] + } +} + +impl AsRef<[u8]> for packed::Backing { + fn as_ref(&self) -> &[u8] { + match self { + packed::Backing::InMemory(data) => data, + packed::Backing::Mapped(map) => map, + } + } +} + +/// +pub mod open { + use std::path::PathBuf; + + use memmap2::Mmap; + + use crate::store_impl::packed; + + /// Initialization + impl packed::Buffer { + /// Open the file at `path` and map it into memory if the file size is larger than `use_memory_map_if_larger_than_bytes`. + /// + /// In order to allow fast lookups and optimizations, the contents of the packed refs must be sorted. + /// If that's not the case, they will be sorted on the fly with the data being written into a memory buffer. + pub fn open(path: impl Into<PathBuf>, use_memory_map_if_larger_than_bytes: u64) -> Result<Self, Error> { + let path = path.into(); + let (backing, offset) = { + let backing = if std::fs::metadata(&path)?.len() <= use_memory_map_if_larger_than_bytes { + packed::Backing::InMemory(std::fs::read(&path)?) + } else { + packed::Backing::Mapped( + // SAFETY: we have to take the risk of somebody changing the file underneath. Git never writes into the same file. + #[allow(unsafe_code)] + unsafe { + Mmap::map(&std::fs::File::open(&path)?)? + }, + ) + }; + + let (offset, sorted) = { + let data = backing.as_ref(); + if *data.first().unwrap_or(&b' ') == b'#' { + let (records, header) = packed::decode::header::<()>(data).map_err(|_| Error::HeaderParsing)?; + let offset = records.as_ptr() as usize - data.as_ptr() as usize; + (offset, header.sorted) + } else { + (0, false) + } + }; + + if !sorted { + // this implementation is likely slower than what git does, but it's less code, too. + let mut entries = packed::Iter::new(&backing.as_ref()[offset..])?.collect::<Result<Vec<_>, _>>()?; + entries.sort_by_key(|e| e.name.as_bstr()); + let mut serialized = Vec::<u8>::new(); + for entry in entries { + serialized.extend_from_slice(entry.target); + serialized.push(b' '); + serialized.extend_from_slice(entry.name.as_bstr()); + serialized.push(b'\n'); + if let Some(object) = entry.object { + serialized.push(b'^'); + serialized.extend_from_slice(object); + serialized.push(b'\n'); + } + } + (Backing::InMemory(serialized), 0) + } else { + (backing, offset) + } + }; + Ok(packed::Buffer { + offset, + data: backing, + path, + }) + } + } + + mod error { + use crate::packed; + + /// The error returned by [`open()`][super::packed::Buffer::open()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("The packed-refs file did not have a header or wasn't sorted and could not be iterated")] + Iter(#[from] packed::iter::Error), + #[error("The header could not be parsed, even though first line started with '#'")] + HeaderParsing, + #[error("The buffer could not be opened or read")] + Io(#[from] std::io::Error), + } + } + pub use error::Error; + + use crate::packed::Backing; +} diff --git a/vendor/gix-ref/src/store/packed/decode.rs b/vendor/gix-ref/src/store/packed/decode.rs new file mode 100644 index 000000000..f8825459e --- /dev/null +++ b/vendor/gix-ref/src/store/packed/decode.rs @@ -0,0 +1,83 @@ +use std::convert::TryInto; + +use gix_object::bstr::{BStr, ByteSlice}; +use nom::{ + bytes::complete::{tag, take_while}, + combinator::{map, map_res, opt}, + error::{FromExternalError, ParseError}, + sequence::{delimited, preceded, terminated, tuple}, + IResult, +}; + +use crate::{ + parse::{hex_hash, newline}, + store_impl::packed, +}; + +#[derive(Debug, PartialEq, Eq)] +enum Peeled { + Unspecified, + Partial, + Fully, +} + +/// Information parsed from the header of a packed ref file +#[derive(Debug, PartialEq, Eq)] +pub struct Header { + peeled: Peeled, + pub sorted: bool, +} + +impl Default for Header { + fn default() -> Self { + Header { + peeled: Peeled::Unspecified, + sorted: false, + } + } +} + +fn until_newline<'a, E>(input: &'a [u8]) -> IResult<&'a [u8], &'a BStr, E> +where + E: ParseError<&'a [u8]>, +{ + map( + terminated(take_while(|b: u8| b != b'\r' && b != b'\n'), newline), + |not_newline| not_newline.as_bstr(), + )(input) +} + +pub fn header<'a, E>(input: &'a [u8]) -> IResult<&'a [u8], Header, E> +where + E: ParseError<&'a [u8]>, +{ + let (rest, traits) = preceded(tag(b"# pack-refs with: "), until_newline)(input)?; + + let mut peeled = Peeled::Unspecified; + let mut sorted = false; + for token in traits.as_bstr().split_str(b" ") { + if token == b"fully-peeled" { + peeled = Peeled::Fully; + } else if token == b"peeled" { + peeled = Peeled::Partial; + } else if token == b"sorted" { + sorted = true; + } + } + + Ok((rest, Header { peeled, sorted })) +} + +pub fn reference<'a, E: ParseError<&'a [u8]> + FromExternalError<&'a [u8], crate::name::Error>>( + input: &'a [u8], +) -> IResult<&'a [u8], packed::Reference<'a>, E> { + let (input, (target, name)) = tuple(( + terminated(hex_hash, tag(b" ")), + map_res(until_newline, TryInto::try_into), + ))(input)?; + let (rest, object) = opt(delimited(tag(b"^"), hex_hash, newline))(input)?; + Ok((rest, packed::Reference { name, target, object })) +} + +#[cfg(test)] +mod tests; diff --git a/vendor/gix-ref/src/store/packed/decode/tests.rs b/vendor/gix-ref/src/store/packed/decode/tests.rs new file mode 100644 index 000000000..6c8f315c1 --- /dev/null +++ b/vendor/gix-ref/src/store/packed/decode/tests.rs @@ -0,0 +1,125 @@ +type Result = std::result::Result<(), Box<dyn std::error::Error>>; + +mod reference { + use nom::error::VerboseError; + + use super::Result; + use crate::{ + store_impl::{packed, packed::decode}, + FullNameRef, + }; + + /// Convert a hexadecimal hash into its corresponding `ObjectId` or _panic_. + fn hex_to_id(hex: &str) -> gix_hash::ObjectId { + gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex") + } + + #[test] + fn invalid() { + assert!(decode::reference::<()>(b"# what looks like a comment",).is_err()); + assert!( + decode::reference::<()>(b"^e9cdc958e7ce2290e2d7958cdb5aa9323ef35d37\n",).is_err(), + "lonely peel" + ); + } + + #[test] + fn two_refs_in_a_row() -> Result { + let input: &[u8] = b"d53c4b0f91f1b29769c9430f2d1c0bcab1170c75 refs/heads/alternates-after-packs-and-loose +^e9cdc958e7ce2290e2d7958cdb5aa9323ef35d37\neaae9c1bc723209d793eb93f5587fa2604d5cd92 refs/heads/avoid-double-lookup\n"; + let (input, parsed) = decode::reference::<VerboseError<_>>(input)?; + + assert_eq!( + parsed, + packed::Reference { + name: FullNameRef::new_unchecked("refs/heads/alternates-after-packs-and-loose".into()), + target: "d53c4b0f91f1b29769c9430f2d1c0bcab1170c75".into(), + object: Some("e9cdc958e7ce2290e2d7958cdb5aa9323ef35d37".into()) + } + ); + assert_eq!(parsed.target(), hex_to_id("d53c4b0f91f1b29769c9430f2d1c0bcab1170c75")); + assert_eq!(parsed.object(), hex_to_id("e9cdc958e7ce2290e2d7958cdb5aa9323ef35d37")); + + let (input, parsed) = decode::reference::<VerboseError<_>>(input)?; + assert!(input.is_empty(), "exhausted"); + assert_eq!( + parsed.name, + FullNameRef::new_unchecked("refs/heads/avoid-double-lookup".into()) + ); + assert_eq!(parsed.target, "eaae9c1bc723209d793eb93f5587fa2604d5cd92"); + assert!(parsed.object.is_none()); + Ok(()) + } +} + +mod header { + use gix_object::bstr::ByteSlice; + use gix_testtools::to_bstr_err; + + use super::Result; + use crate::store_impl::packed::{ + decode, + decode::{Header, Peeled}, + }; + + #[test] + fn invalid() { + assert!( + decode::header::<()>(b"# some user comment").is_err(), + "something the user put there" + ); + assert!(decode::header::<()>(b"# pack-refs: ").is_err(), "looks right but isn't"); + assert!( + decode::header::<()>(b" # pack-refs with: ").is_err(), + "does not start with #" + ); + } + + #[test] + fn valid_fully_peeled_stored() -> Result { + let input: &[u8] = b"# pack-refs with: peeled fully-peeled sorted \nsomething else"; + let (rest, header) = decode::header::<nom::error::VerboseError<_>>(input).map_err(to_bstr_err)?; + + assert_eq!(rest.as_bstr(), "something else", "remainder starts after newline"); + assert_eq!( + header, + Header { + peeled: Peeled::Fully, + sorted: true + } + ); + Ok(()) + } + + #[test] + fn valid_peeled_unsorted() -> Result { + let input: &[u8] = b"# pack-refs with: peeled\n"; + let (rest, header) = decode::header::<()>(input)?; + + assert!(rest.is_empty()); + assert_eq!( + header, + Header { + peeled: Peeled::Partial, + sorted: false + } + ); + Ok(()) + } + + #[test] + fn valid_empty() -> Result { + let input: &[u8] = b"# pack-refs with: \n"; + let (rest, header) = decode::header::<()>(input)?; + + assert!(rest.is_empty()); + assert_eq!( + header, + Header { + peeled: Peeled::Unspecified, + sorted: false + } + ); + Ok(()) + } +} diff --git a/vendor/gix-ref/src/store/packed/find.rs b/vendor/gix-ref/src/store/packed/find.rs new file mode 100644 index 000000000..abd35dfe2 --- /dev/null +++ b/vendor/gix-ref/src/store/packed/find.rs @@ -0,0 +1,154 @@ +use std::convert::TryInto; + +use gix_object::bstr::{BStr, BString, ByteSlice}; + +use crate::{store_impl::packed, FullNameRef, PartialNameRef}; + +/// packed-refs specific functionality +impl packed::Buffer { + /// Find a reference with the given `name` and return it. + /// + /// Note that it will look it up verbatim and does not deal with namespaces or special prefixes like + /// `main-worktree/` or `worktrees/<name>/`, as this is left to the caller. + pub fn try_find<'a, Name, E>(&self, name: Name) -> Result<Option<packed::Reference<'_>>, Error> + where + Name: TryInto<&'a PartialNameRef, Error = E>, + Error: From<E>, + { + let name = name.try_into()?; + let mut buf = BString::default(); + for inbetween in &["", "tags", "heads", "remotes"] { + let (name, was_absolute) = if name.looks_like_full_name() { + let name = FullNameRef::new_unchecked(name.as_bstr()); + let name = match transform_full_name_for_lookup(name) { + None => return Ok(None), + Some(name) => name, + }; + (name, true) + } else { + let full_name = name.construct_full_name_ref(true, inbetween, &mut buf); + (full_name, false) + }; + match self.try_find_full_name(name)? { + Some(r) => return Ok(Some(r)), + None if was_absolute => return Ok(None), + None => continue, + } + } + Ok(None) + } + + pub(crate) fn try_find_full_name(&self, name: &FullNameRef) -> Result<Option<packed::Reference<'_>>, Error> { + match self.binary_search_by(name.as_bstr()) { + Ok(line_start) => Ok(Some( + packed::decode::reference::<()>(&self.as_ref()[line_start..]) + .map_err(|_| Error::Parse)? + .1, + )), + Err((parse_failure, _)) => { + if parse_failure { + Err(Error::Parse) + } else { + Ok(None) + } + } + } + } + + /// Find a reference with the given `name` and return it. + pub fn find<'a, Name, E>(&self, name: Name) -> Result<packed::Reference<'_>, existing::Error> + where + Name: TryInto<&'a PartialNameRef, Error = E>, + Error: From<E>, + { + match self.try_find(name) { + Ok(Some(r)) => Ok(r), + Ok(None) => Err(existing::Error::NotFound), + Err(err) => Err(existing::Error::Find(err)), + } + } + + /// Perform a binary search where `Ok(pos)` is the beginning of the line that matches `name` perfectly and `Err(pos)` + /// is the beginning of the line at which `name` could be inserted to still be in sort order. + pub(in crate::store_impl::packed) fn binary_search_by(&self, full_name: &BStr) -> Result<usize, (bool, usize)> { + let a = self.as_ref(); + let search_start_of_record = |ofs: usize| { + a[..ofs] + .rfind(b"\n") + .and_then(|pos| { + let candidate = pos + 1; + a.get(candidate).and_then(|b| { + if *b == b'^' { + a[..pos].rfind(b"\n").map(|pos| pos + 1) + } else { + Some(candidate) + } + }) + }) + .unwrap_or(0) + }; + let mut encountered_parse_failure = false; + a.binary_search_by_key(&full_name.as_ref(), |b: &u8| { + let ofs = b as *const u8 as usize - a.as_ptr() as usize; + let line = &a[search_start_of_record(ofs)..]; + packed::decode::reference::<()>(line) + .map(|(_rest, r)| r.name.as_bstr().as_bytes()) + .map_err(|err| { + encountered_parse_failure = true; + err + }) + .unwrap_or(&[]) + }) + .map(search_start_of_record) + .map_err(|pos| (encountered_parse_failure, search_start_of_record(pos))) + } +} + +mod error { + use std::convert::Infallible; + + /// The error returned by [`find()`][super::packed::Buffer::find()] + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("The ref name or path is not a valid ref name")] + RefnameValidation(#[from] crate::name::Error), + #[error("The reference could not be parsed")] + Parse, + } + + impl From<Infallible> for Error { + fn from(_: Infallible) -> Self { + unreachable!("this impl is needed to allow passing a known valid partial path as parameter") + } + } +} +pub use error::Error; + +/// +pub mod existing { + + /// The error returned by [`find_existing()`][super::packed::Buffer::find()] + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("The find operation failed")] + Find(#[from] super::Error), + #[error("The reference did not exist even though that was expected")] + NotFound, + } +} + +pub(crate) fn transform_full_name_for_lookup(name: &FullNameRef) -> Option<&FullNameRef> { + match name.category_and_short_name() { + Some((c, sn)) => { + use crate::Category::*; + Some(match c { + MainRef | LinkedRef { .. } => FullNameRef::new_unchecked(sn), + Tag | RemoteBranch | LocalBranch | Bisect | Rewritten | Note => name, + MainPseudoRef | PseudoRef | LinkedPseudoRef { .. } | WorktreePrivate => return None, + }) + } + None => Some(name), + } +} diff --git a/vendor/gix-ref/src/store/packed/iter.rs b/vendor/gix-ref/src/store/packed/iter.rs new file mode 100644 index 000000000..d9c49956b --- /dev/null +++ b/vendor/gix-ref/src/store/packed/iter.rs @@ -0,0 +1,117 @@ +use gix_object::bstr::{BString, ByteSlice}; + +use crate::store_impl::{packed, packed::decode}; + +/// packed-refs specific functionality +impl packed::Buffer { + /// Return an iterator of references stored in this packed refs buffer, ordered by reference name. + /// + /// # Note + /// + /// There is no namespace support in packed iterators. It can be emulated using `iter_prefixed(…)`. + pub fn iter(&self) -> Result<packed::Iter<'_>, packed::iter::Error> { + packed::Iter::new(self.as_ref()) + } + + /// Return an iterator yielding only references matching the given prefix, ordered by reference name. + pub fn iter_prefixed(&self, prefix: impl Into<BString>) -> Result<packed::Iter<'_>, packed::iter::Error> { + let prefix = prefix.into(); + let first_record_with_prefix = self.binary_search_by(prefix.as_bstr()).unwrap_or_else(|(_, pos)| pos); + packed::Iter::new_with_prefix(&self.as_ref()[first_record_with_prefix..], Some(prefix)) + } +} + +impl<'a> Iterator for packed::Iter<'a> { + type Item = Result<packed::Reference<'a>, Error>; + + fn next(&mut self) -> Option<Self::Item> { + if self.cursor.is_empty() { + return None; + } + + match decode::reference::<()>(self.cursor) { + Ok((rest, reference)) => { + self.cursor = rest; + self.current_line += 1; + if let Some(ref prefix) = self.prefix { + if !reference.name.as_bstr().starts_with_str(prefix) { + self.cursor = &[]; + return None; + } + } + Some(Ok(reference)) + } + Err(_) => { + let (failed_line, next_cursor) = self + .cursor + .find_byte(b'\n') + .map_or((self.cursor, &[][..]), |pos| self.cursor.split_at(pos + 1)); + self.cursor = next_cursor; + let line_number = self.current_line; + self.current_line += 1; + + Some(Err(Error::Reference { + invalid_line: failed_line + .get(..failed_line.len().saturating_sub(1)) + .unwrap_or(failed_line) + .into(), + line_number, + })) + } + } + } +} + +impl<'a> packed::Iter<'a> { + /// Return a new iterator after successfully parsing the possibly existing first line of the given `packed` refs buffer. + pub fn new(packed: &'a [u8]) -> Result<Self, Error> { + Self::new_with_prefix(packed, None) + } + + /// Returns an iterators whose references will only match the given prefix. + /// + /// It assumes that the underlying `packed` buffer is indeed sorted + pub(in crate::store_impl::packed) fn new_with_prefix( + packed: &'a [u8], + prefix: Option<BString>, + ) -> Result<Self, Error> { + if packed.is_empty() { + Ok(packed::Iter { + cursor: packed, + prefix, + current_line: 1, + }) + } else if packed[0] == b'#' { + let (refs, _header) = decode::header::<()>(packed).map_err(|_| Error::Header { + invalid_first_line: packed.lines().next().unwrap_or(packed).into(), + })?; + Ok(packed::Iter { + cursor: refs, + prefix, + current_line: 2, + }) + } else { + Ok(packed::Iter { + cursor: packed, + prefix, + current_line: 1, + }) + } + } +} + +mod error { + use gix_object::bstr::BString; + + /// The error returned by [`Iter`][super::packed::Iter], + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("The header existed but could not be parsed: {invalid_first_line:?}")] + Header { invalid_first_line: BString }, + #[error("Invalid reference in line {line_number}: {invalid_line:?}")] + Reference { invalid_line: BString, line_number: usize }, + } +} + +pub use error::Error; diff --git a/vendor/gix-ref/src/store/packed/mod.rs b/vendor/gix-ref/src/store/packed/mod.rs new file mode 100644 index 000000000..53a077414 --- /dev/null +++ b/vendor/gix-ref/src/store/packed/mod.rs @@ -0,0 +1,93 @@ +use std::path::PathBuf; + +use gix_hash::ObjectId; +use gix_object::bstr::{BStr, BString}; +use memmap2::Mmap; + +use crate::{file, transaction::RefEdit, FullNameRef}; + +#[derive(Debug)] +enum Backing { + /// The buffer is loaded entirely in memory, along with the `offset` to the first record past the header. + InMemory(Vec<u8>), + /// The buffer is mapping the file on disk, along with the offset to the first record past the header + Mapped(Mmap), +} + +/// A buffer containing a packed-ref file that is either memory mapped or fully in-memory depending on a cutoff. +/// +/// The buffer is guaranteed to be sorted as per the packed-ref rules which allows some operations to be more efficient. +#[derive(Debug)] +pub struct Buffer { + data: Backing, + /// The offset to the first record, how many bytes to skip past the header + offset: usize, + /// The path from which we were loaded + path: PathBuf, +} + +struct Edit { + inner: RefEdit, + peeled: Option<ObjectId>, +} + +/// A transaction for editing packed references +pub(crate) struct Transaction { + buffer: Option<file::packed::SharedBufferSnapshot>, + edits: Option<Vec<Edit>>, + lock: Option<gix_lock::File>, + #[allow(dead_code)] // It just has to be kept alive, hence no reads + closed_lock: Option<gix_lock::Marker>, +} + +/// A reference as parsed from the `packed-refs` file +#[derive(Debug, PartialEq, Eq)] +pub struct Reference<'a> { + /// The validated full name of the reference. + pub name: &'a FullNameRef, + /// The target object id of the reference, hex encoded. + pub target: &'a BStr, + /// The fully peeled object id, hex encoded, that the ref is ultimately pointing to + /// i.e. when all indirections are removed. + pub object: Option<&'a BStr>, +} + +impl<'a> Reference<'a> { + /// Decode the target as object + pub fn target(&self) -> ObjectId { + gix_hash::ObjectId::from_hex(self.target).expect("parser validation") + } + + /// Decode the object this reference is ultimately pointing to. Note that this is + /// the [`target()`][Reference::target()] if this is not a fully peeled reference like a tag. + pub fn object(&self) -> ObjectId { + self.object.map_or_else( + || self.target(), + |id| ObjectId::from_hex(id).expect("parser validation"), + ) + } +} + +/// An iterator over references in a packed refs file +pub struct Iter<'a> { + /// The position at which to parse the next reference + cursor: &'a [u8], + /// The next line, starting at 1 + current_line: usize, + /// If set, references returned will match the prefix, the first failed match will stop all iteration. + prefix: Option<BString>, +} + +mod decode; + +/// +pub mod iter; + +/// +pub mod buffer; + +/// +pub mod find; + +/// +pub mod transaction; diff --git a/vendor/gix-ref/src/store/packed/transaction.rs b/vendor/gix-ref/src/store/packed/transaction.rs new file mode 100644 index 000000000..26cc84b9b --- /dev/null +++ b/vendor/gix-ref/src/store/packed/transaction.rs @@ -0,0 +1,267 @@ +use std::{fmt::Formatter, io::Write}; + +use crate::{ + file, + store_impl::{file::transaction::FindObjectFn, packed, packed::Edit}, + transaction::{Change, RefEdit}, + Target, +}; + +pub(crate) const HEADER_LINE: &[u8] = b"# pack-refs with: peeled fully-peeled sorted \n"; + +/// Access and instantiation +impl packed::Transaction { + pub(crate) fn new_from_pack_and_lock( + buffer: Option<file::packed::SharedBufferSnapshot>, + lock: gix_lock::File, + ) -> Self { + packed::Transaction { + buffer, + edits: None, + lock: Some(lock), + closed_lock: None, + } + } +} + +impl std::fmt::Debug for packed::Transaction { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("packed::Transaction") + .field("edits", &self.edits.as_ref().map(|e| e.len())) + .field("lock", &self.lock) + .finish_non_exhaustive() + } +} + +/// Access +impl packed::Transaction { + /// Returns our packed buffer + pub fn buffer(&self) -> Option<&packed::Buffer> { + self.buffer.as_ref().map(|b| &***b) + } +} + +/// Lifecycle +impl packed::Transaction { + /// Prepare the transaction by checking all edits for applicability. + pub fn prepare( + mut self, + edits: impl IntoIterator<Item = RefEdit>, + find: &mut FindObjectFn<'_>, + ) -> Result<Self, prepare::Error> { + assert!(self.edits.is_none(), "BUG: cannot call prepare(…) more than once"); + let buffer = &self.buffer; + // Remove all edits which are deletions that aren't here in the first place + let mut edits: Vec<Edit> = edits + .into_iter() + .filter(|edit| { + if let Change::Delete { .. } = edit.change { + buffer.as_ref().map_or(true, |b| b.find(edit.name.as_ref()).is_ok()) + } else { + true + } + }) + .map(|change| Edit { + inner: change, + peeled: None, + }) + .collect(); + + let mut buf = Vec::new(); + for edit in edits.iter_mut() { + if let Change::Update { + new: Target::Peeled(new), + .. + } = edit.inner.change + { + let mut next_id = new; + edit.peeled = loop { + let kind = find(next_id, &mut buf)?; + match kind { + Some(kind) if kind == gix_object::Kind::Tag => { + next_id = gix_object::TagRefIter::from_bytes(&buf).target_id().map_err(|_| { + prepare::Error::Resolve( + format!("Couldn't get target object id from tag {next_id}").into(), + ) + })?; + } + Some(_) => { + break if next_id == new { None } else { Some(next_id) }; + } + None => { + return Err(prepare::Error::Resolve( + format!("Couldn't find object with id {next_id}").into(), + )) + } + } + }; + } + } + + if edits.is_empty() { + self.closed_lock = self + .lock + .take() + .map(|l| l.close()) + .transpose() + .map_err(prepare::Error::CloseLock)?; + } else { + // NOTE that we don't do any additional checks here but apply all edits unconditionally. + // This is because this transaction system is internal and will be used correctly from the + // loose ref store transactions, which do the necessary checking. + } + self.edits = Some(edits); + Ok(self) + } + + /// Commit the prepared transaction. + /// + /// Please note that actual edits invalidated existing packed buffers. + /// Note: There is the potential to write changes into memory and return such a packed-refs buffer for reuse. + pub fn commit(self) -> Result<(), commit::Error> { + let mut edits = self.edits.expect("BUG: cannot call commit() before prepare(…)"); + if edits.is_empty() { + return Ok(()); + } + + let mut file = self.lock.expect("a write lock for applying changes"); + let refs_sorted: Box<dyn Iterator<Item = Result<packed::Reference<'_>, packed::iter::Error>>> = + match self.buffer.as_ref() { + Some(buffer) => Box::new(buffer.iter()?), + None => Box::new(std::iter::empty()), + }; + + let mut refs_sorted = refs_sorted.peekable(); + + edits.sort_by(|l, r| l.inner.name.as_bstr().cmp(r.inner.name.as_bstr())); + let mut peekable_sorted_edits = edits.iter().peekable(); + + file.with_mut(|f| f.write_all(HEADER_LINE))?; + + let mut num_written_lines = 0; + loop { + match (refs_sorted.peek(), peekable_sorted_edits.peek()) { + (Some(Err(_)), _) => { + let err = refs_sorted.next().expect("next").expect_err("err"); + return Err(commit::Error::Iteration(err)); + } + (None, None) => { + break; + } + (Some(Ok(_)), None) => { + let pref = refs_sorted.next().expect("next").expect("no err"); + num_written_lines += 1; + file.with_mut(|out| write_packed_ref(out, pref))?; + } + (Some(Ok(pref)), Some(edit)) => { + use std::cmp::Ordering::*; + match pref.name.as_bstr().cmp(edit.inner.name.as_bstr()) { + Less => { + let pref = refs_sorted.next().expect("next").expect("valid"); + num_written_lines += 1; + file.with_mut(|out| write_packed_ref(out, pref))?; + } + Greater => { + let edit = peekable_sorted_edits.next().expect("next"); + file.with_mut(|out| write_edit(out, edit, &mut num_written_lines))?; + } + Equal => { + let _pref = refs_sorted.next().expect("next").expect("valid"); + let edit = peekable_sorted_edits.next().expect("next"); + file.with_mut(|out| write_edit(out, edit, &mut num_written_lines))?; + } + } + } + (None, Some(_)) => { + let edit = peekable_sorted_edits.next().expect("next"); + file.with_mut(|out| write_edit(out, edit, &mut num_written_lines))?; + } + } + } + + if num_written_lines == 0 { + std::fs::remove_file(file.resource_path())?; + } else { + file.commit()?; + } + drop(refs_sorted); + Ok(()) + } +} + +fn write_packed_ref(mut out: impl std::io::Write, pref: packed::Reference<'_>) -> std::io::Result<()> { + write!(out, "{} ", pref.target)?; + out.write_all(pref.name.as_bstr())?; + out.write_all(b"\n")?; + if let Some(object) = pref.object { + writeln!(out, "^{object}")?; + } + Ok(()) +} + +fn write_edit(mut out: impl std::io::Write, edit: &Edit, lines_written: &mut i32) -> std::io::Result<()> { + match edit.inner.change { + Change::Delete { .. } => {} + Change::Update { + new: Target::Peeled(target_oid), + .. + } => { + write!(out, "{target_oid} ")?; + out.write_all(edit.inner.name.as_bstr())?; + out.write_all(b"\n")?; + if let Some(object) = edit.peeled { + writeln!(out, "^{object}")?; + } + *lines_written += 1; + } + Change::Update { + new: Target::Symbolic(_), + .. + } => unreachable!("BUG: packed refs cannot contain symbolic refs, catch that in prepare(…)"), + } + Ok(()) +} + +/// Convert this buffer to be used as the basis for a transaction. +pub(crate) fn buffer_into_transaction( + buffer: file::packed::SharedBufferSnapshot, + lock_mode: gix_lock::acquire::Fail, +) -> Result<packed::Transaction, gix_lock::acquire::Error> { + let lock = gix_lock::File::acquire_to_update_resource(&buffer.path, lock_mode, None)?; + Ok(packed::Transaction { + buffer: Some(buffer), + lock: Some(lock), + closed_lock: None, + edits: None, + }) +} + +/// +pub mod prepare { + /// The error used in [`Transaction::prepare(…)`][crate::file::Transaction::prepare()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("Could not close a lock which won't ever be committed")] + CloseLock(#[from] std::io::Error), + #[error("The lookup of an object failed while peeling it")] + Resolve(#[from] Box<dyn std::error::Error + Send + Sync + 'static>), + } +} + +/// +pub mod commit { + use crate::store_impl::packed; + + /// The error used in [`Transaction::commit(…)`][crate::file::Transaction::commit()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("Changes to the resource could not be committed")] + Commit(#[from] gix_lock::commit::Error<gix_lock::File>), + #[error("Some references in the packed refs buffer could not be parsed")] + Iteration(#[from] packed::iter::Error), + #[error("Failed to write a ref line to the packed ref file")] + Io(#[from] std::io::Error), + } +} |