summaryrefslogtreecommitdiffstats
path: root/vendor/gix-ref/src/store/packed
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:41 +0000
commit10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 (patch)
treebdffd5d80c26cf4a7a518281a204be1ace85b4c1 /vendor/gix-ref/src/store/packed
parentReleasing progress-linux version 1.70.0+dfsg1-9~progress7.99u1. (diff)
downloadrustc-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.rs105
-rw-r--r--vendor/gix-ref/src/store/packed/decode.rs83
-rw-r--r--vendor/gix-ref/src/store/packed/decode/tests.rs125
-rw-r--r--vendor/gix-ref/src/store/packed/find.rs154
-rw-r--r--vendor/gix-ref/src/store/packed/iter.rs117
-rw-r--r--vendor/gix-ref/src/store/packed/mod.rs93
-rw-r--r--vendor/gix-ref/src/store/packed/transaction.rs267
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),
+ }
+}