diff --git a/gix-index/src/access/mod.rs b/gix-index/src/access/mod.rs index 9a66fa2b08d..8cf9cd57610 100644 --- a/gix-index/src/access/mod.rs +++ b/gix-index/src/access/mod.rs @@ -518,7 +518,8 @@ impl State { /// Physically remove all entries for which `should_remove(idx, path, entry)` returns `true`, traversing them from first to last. /// - /// Note that the memory used for the removed entries paths is not freed, as it's append-only. + /// Note that the memory used for the removed entries paths is not freed, as it's append-only, and + /// that some extensions might refer to paths which are now deleted. /// /// ### Performance /// @@ -534,6 +535,16 @@ impl State { res }); } + + /// Physically remove the entry at `index`, or panic if the entry didn't exist. + /// + /// This call is typically made after looking up `index`, so it's clear that it will not panic. + /// + /// Note that the memory used for the removed entries paths is not freed, as it's append-only, and + /// that some extensions might refer to paths which are now deleted. + pub fn remove_entry_at_index(&mut self, index: usize) -> Entry { + self.entries.remove(index) + } } /// Extensions @@ -542,6 +553,10 @@ impl State { pub fn tree(&self) -> Option<&extension::Tree> { self.tree.as_ref() } + /// Remove the `tree` extension. + pub fn remove_tree(&mut self) -> Option { + self.tree.take() + } /// Access the `link` extension. pub fn link(&self) -> Option<&extension::Link> { self.link.as_ref() @@ -550,6 +565,10 @@ impl State { pub fn resolve_undo(&self) -> Option<&extension::resolve_undo::Paths> { self.resolve_undo.as_ref() } + /// Remove the resolve-undo extension. + pub fn remove_resolve_undo(&mut self) -> Option { + self.resolve_undo.take() + } /// Obtain the untracked extension. pub fn untracked(&self) -> Option<&extension::UntrackedCache> { self.untracked.as_ref() diff --git a/gix-index/src/entry/mod.rs b/gix-index/src/entry/mod.rs index 3f5ba509b27..41f167bb902 100644 --- a/gix-index/src/entry/mod.rs +++ b/gix-index/src/entry/mod.rs @@ -107,9 +107,21 @@ mod access { mod _impls { use std::cmp::Ordering; + use crate::{entry, Entry, State}; use bstr::BStr; - - use crate::{Entry, State}; + use gix_object::tree::EntryKind; + + impl From for entry::Mode { + fn from(value: EntryKind) -> Self { + match value { + EntryKind::Tree => entry::Mode::DIR, + EntryKind::Blob => entry::Mode::FILE, + EntryKind::BlobExecutable => entry::Mode::FILE_EXECUTABLE, + EntryKind::Link => entry::Mode::SYMLINK, + EntryKind::Commit => entry::Mode::COMMIT, + } + } + } impl Entry { /// Compare one entry to another by their path, by comparing only their common path portion byte by byte, then resorting to diff --git a/gix-index/tests/index/access.rs b/gix-index/tests/index/access.rs index 5622d5b96c2..27ffd24ff69 100644 --- a/gix-index/tests/index/access.rs +++ b/gix-index/tests/index/access.rs @@ -214,6 +214,18 @@ fn remove_entries() { file.remove_entries(|_, _, _| unreachable!("should not be called")); } +#[test] +fn remove_entry_at_index() { + let mut file = Fixture::Loose("conflicting-file").open(); + + file.remove_entry_at_index(0); + assert_eq!(file.entries().len(), 2); + file.remove_entry_at_index(0); + assert_eq!(file.entries().len(), 1); + file.remove_entry_at_index(0); + assert_eq!(file.entries().len(), 0); +} + #[test] fn sort_entries() { let mut file = Fixture::Generated("v4_more_files_IEOT").open(); diff --git a/gix-object/src/blob.rs b/gix-object/src/blob.rs index 455155ce685..e9a9702ca4a 100644 --- a/gix-object/src/blob.rs +++ b/gix-object/src/blob.rs @@ -44,4 +44,9 @@ impl BlobRef<'_> { pub fn from_bytes(data: &[u8]) -> Result, Infallible> { Ok(BlobRef { data }) } + + /// Clone the data in this instance by allocating a new vector for a fully owned blob. + pub fn into_owned(self) -> Blob { + self.into() + } } diff --git a/gix-object/src/commit/mod.rs b/gix-object/src/commit/mod.rs index 17dc12bebd6..8cc49647158 100644 --- a/gix-object/src/commit/mod.rs +++ b/gix-object/src/commit/mod.rs @@ -5,6 +5,9 @@ use winnow::prelude::*; use crate::{Commit, CommitRef, TagRef}; +/// The well-known field name for gpg signatures. +pub const SIGNATURE_FIELD_NAME: &str = "gpgsig"; + mod decode; /// pub mod message; @@ -84,7 +87,7 @@ impl<'a> CommitRef<'a> { } /// Returns a convenient iterator over all extra headers. - pub fn extra_headers(&self) -> crate::commit::ExtraHeaders> { + pub fn extra_headers(&self) -> ExtraHeaders> { ExtraHeaders::new(self.extra_headers.iter().map(|(k, v)| (*k, v.as_ref()))) } @@ -113,6 +116,19 @@ impl<'a> CommitRef<'a> { } } +/// Conversion +impl CommitRef<'_> { + /// Copy all fields of this instance into a fully owned commit, consuming this instance. + pub fn into_owned(self) -> Commit { + self.into() + } + + /// Copy all fields of this instance into a fully owned commit, internally cloning this instance. + pub fn to_owned(self) -> Commit { + self.clone().into() + } +} + impl Commit { /// Returns a convenient iterator over all extra headers. pub fn extra_headers(&self) -> ExtraHeaders> { @@ -134,16 +150,26 @@ where pub fn new(iter: I) -> Self { ExtraHeaders { inner: iter } } + /// Find the _value_ of the _first_ header with the given `name`. pub fn find(mut self, name: &str) -> Option<&'a BStr> { self.inner .find_map(move |(k, v)| if k == name.as_bytes().as_bstr() { Some(v) } else { None }) } + + /// Find the entry index with the given name, or return `None` if unavailable. + pub fn find_pos(self, name: &str) -> Option { + self.inner + .enumerate() + .find_map(|(pos, (field, _value))| (field == name).then_some(pos)) + } + /// Return an iterator over all _values_ of headers with the given `name`. pub fn find_all(self, name: &'a str) -> impl Iterator { self.inner .filter_map(move |(k, v)| if k == name.as_bytes().as_bstr() { Some(v) } else { None }) } + /// Return an iterator over all git mergetags. /// /// A merge tag is a tag object embedded within the respective header field of a commit, making @@ -154,6 +180,6 @@ where /// Return the cryptographic signature provided by gpg/pgp verbatim. pub fn pgp_signature(self) -> Option<&'a BStr> { - self.find("gpgsig") + self.find(SIGNATURE_FIELD_NAME) } } diff --git a/gix-object/src/lib.rs b/gix-object/src/lib.rs index 8c94f9e6bf9..398374bee16 100644 --- a/gix-object/src/lib.rs +++ b/gix-object/src/lib.rs @@ -58,7 +58,7 @@ pub enum Kind { Commit, Tag, } -/// A chunk of any [`data`][BlobRef::data]. +/// A chunk of any [`data`](BlobRef::data). #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BlobRef<'a> { @@ -66,7 +66,7 @@ pub struct BlobRef<'a> { pub data: &'a [u8], } -/// A mutable chunk of any [`data`][Blob::data]. +/// A mutable chunk of any [`data`](Blob::data). #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Blob { @@ -74,7 +74,7 @@ pub struct Blob { pub data: Vec, } -/// A git commit parsed using [`from_bytes()`][CommitRef::from_bytes()]. +/// A git commit parsed using [`from_bytes()`](CommitRef::from_bytes()). /// /// A commit encapsulates information about a point in time at which the state of the repository is recorded, usually after a /// change which is documented in the commit `message`. @@ -83,18 +83,18 @@ pub struct Blob { pub struct CommitRef<'a> { /// HEX hash of tree object we point to. Usually 40 bytes long. /// - /// Use [`tree()`][CommitRef::tree()] to obtain a decoded version of it. + /// Use [`tree()`](CommitRef::tree()) to obtain a decoded version of it. #[cfg_attr(feature = "serde", serde(borrow))] pub tree: &'a BStr, /// HEX hash of each parent commit. Empty for first commit in repository. pub parents: SmallVec<[&'a BStr; 1]>, /// Who wrote this commit. Name and email might contain whitespace and are not trimmed to ensure round-tripping. /// - /// Use the [`author()`][CommitRef::author()] method to received a trimmed version of it. + /// Use the [`author()`](CommitRef::author()) method to received a trimmed version of it. pub author: gix_actor::SignatureRef<'a>, /// Who committed this commit. Name and email might contain whitespace and are not trimmed to ensure round-tripping. /// - /// Use the [`committer()`][CommitRef::committer()] method to received a trimmed version of it. + /// Use the [`committer()`](CommitRef::committer()) method to received a trimmed version of it. /// /// This may be different from the `author` in case the author couldn't write to the repository themselves and /// is commonly encountered with contributed commits. @@ -103,7 +103,7 @@ pub struct CommitRef<'a> { pub encoding: Option<&'a BStr>, /// The commit message documenting the change. pub message: &'a BStr, - /// Extra header fields, in order of them being encountered, made accessible with the iterator returned by [`extra_headers()`][CommitRef::extra_headers()]. + /// Extra header fields, in order of them being encountered, made accessible with the iterator returned by [`extra_headers()`](CommitRef::extra_headers()). pub extra_headers: Vec<(&'a BStr, Cow<'a, BStr>)>, } @@ -135,15 +135,15 @@ pub struct Commit { /// The commit message documenting the change. pub message: BString, /// Extra header fields, in order of them being encountered, made accessible with the iterator returned - /// by [`extra_headers()`][Commit::extra_headers()]. + /// by [`extra_headers()`](Commit::extra_headers()). pub extra_headers: Vec<(BString, BString)>, } /// Represents a git tag, commonly indicating a software release. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TagRef<'a> { - /// The hash in hexadecimal being the object this tag points to. Use [`target()`][TagRef::target()] to obtain a byte representation. + /// The hash in hexadecimal being the object this tag points to. Use [`target()`](TagRef::target()) to obtain a byte representation. #[cfg_attr(feature = "serde", serde(borrow))] pub target: &'a BStr, /// The kind of object that `target` points to. @@ -184,13 +184,13 @@ pub struct Tag { pub pgp_signature: Option, } -/// Immutable objects are read-only structures referencing most data from [a byte slice][crate::ObjectRef::from_bytes()]. +/// Immutable objects are read-only structures referencing most data from [a byte slice](ObjectRef::from_bytes()). /// /// Immutable objects are expected to be deserialized from bytes that acts as backing store, and they -/// cannot be mutated or serialized. Instead, one will [convert][crate::ObjectRef::into_owned()] them into their [`mutable`][Object] counterparts +/// cannot be mutated or serialized. Instead, one will [convert](ObjectRef::into_owned()) them into their [`mutable`](Object) counterparts /// which support mutation and serialization. /// -/// An `ObjectRef` is representing [`Trees`][TreeRef], [`Blobs`][BlobRef], [`Commits`][CommitRef], or [`Tags`][TagRef]. +/// An `ObjectRef` is representing [`Trees`](TreeRef), [`Blobs`](BlobRef), [`Commits`](CommitRef), or [`Tags`](TagRef). #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(missing_docs)] @@ -206,10 +206,10 @@ pub enum ObjectRef<'a> { /// /// Mutable objects are Commits, Trees, Blobs and Tags that can be changed and serialized. /// -/// They either created using object [construction][Object] or by [deserializing existing objects][ObjectRef::from_bytes()] -/// and converting these [into mutable copies][ObjectRef::into_owned()] for adjustments. +/// They either created using object [construction](Object) or by [deserializing existing objects](ObjectRef::from_bytes()) +/// and converting these [into mutable copies](ObjectRef::into_owned()) for adjustments. /// -/// An `Object` is representing [`Trees`][Tree], [`Blobs`][Blob], [`Commits`][Commit] or [`Tags`][Tag]. +/// An `Object` is representing [`Trees`](Tree), [`Blobs`](Blob), [`Commits`](Commit), or [`Tags`](Tag). #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(clippy::large_enum_variant, missing_docs)] diff --git a/gix-object/src/tag/mod.rs b/gix-object/src/tag/mod.rs index ecd56c9f7db..a9976dccebc 100644 --- a/gix-object/src/tag/mod.rs +++ b/gix-object/src/tag/mod.rs @@ -23,4 +23,9 @@ impl<'a> TagRef<'a> { pub fn target(&self) -> gix_hash::ObjectId { gix_hash::ObjectId::from_hex(self.target).expect("prior validation") } + + /// Copy all data into a fully-owned instance. + pub fn into_owned(self) -> crate::Tag { + self.into() + } } diff --git a/gix-object/tests/object/commit/from_bytes.rs b/gix-object/tests/object/commit/from_bytes.rs index bf61ade50cf..9a7e0f23842 100644 --- a/gix-object/tests/object/commit/from_bytes.rs +++ b/gix-object/tests/object/commit/from_bytes.rs @@ -1,12 +1,11 @@ -use gix_actor::SignatureRef; -use gix_date::{time::Sign, Time}; -use gix_object::{bstr::ByteSlice, commit::message::body::TrailerRef, CommitRef}; -use smallvec::SmallVec; - use crate::{ commit::{LONG_MESSAGE, MERGE_TAG, SIGNATURE}, fixture_name, linus_signature, signature, }; +use gix_actor::SignatureRef; +use gix_date::{time::Sign, Time}; +use gix_object::{bstr::ByteSlice, commit::message::body::TrailerRef, CommitRef}; +use smallvec::SmallVec; #[test] fn invalid_timestsamp() { @@ -354,7 +353,12 @@ fn newline_right_after_signature_multiline_header() -> crate::Result { let pgp_sig = crate::commit::OTHER_SIGNATURE.as_bstr(); assert_eq!(commit.extra_headers[0].1.as_ref(), pgp_sig); assert_eq!(commit.extra_headers().pgp_signature(), Some(pgp_sig)); - assert_eq!(commit.extra_headers().find("gpgsig"), Some(pgp_sig)); + assert_eq!( + commit.extra_headers().find(gix_object::commit::SIGNATURE_FIELD_NAME), + Some(pgp_sig) + ); + assert_eq!(commit.extra_headers().find_pos("gpgsig"), Some(0)); + assert_eq!(commit.extra_headers().find_pos("something else"), None); assert!(commit.message.starts_with(b"Rollup")); Ok(()) } diff --git a/gix-object/tests/object/tag/mod.rs b/gix-object/tests/object/tag/mod.rs index 923d21d9775..cb6aa857f95 100644 --- a/gix-object/tests/object/tag/mod.rs +++ b/gix-object/tests/object/tag/mod.rs @@ -4,6 +4,7 @@ use gix_object::{bstr::ByteSlice, Kind, TagRef, TagRefIter}; use crate::fixture_name; mod method { + use bstr::ByteSlice; use gix_object::TagRef; use pretty_assertions::assert_eq; @@ -15,6 +16,21 @@ mod method { let tag = TagRef::from_bytes(&fixture)?; assert_eq!(tag.target(), hex_to_id("ffa700b4aca13b80cb6b98a078e7c96804f8e0ec")); assert_eq!(tag.target, "ffa700b4aca13b80cb6b98a078e7c96804f8e0ec".as_bytes()); + + let gix_object::Tag { + target, + target_kind, + name, + tagger, + message, + pgp_signature, + } = tag.into_owned(); + assert_eq!(target.to_string(), tag.target); + assert_eq!(target_kind, tag.target_kind); + assert_eq!(name, tag.name); + assert_eq!(tagger.as_ref().map(|s| s.to_ref()), tag.tagger); + assert_eq!(message, tag.message); + assert_eq!(pgp_signature.as_ref().map(|s| s.as_bstr()), tag.pgp_signature); Ok(()) } } diff --git a/gix/src/repository/object.rs b/gix/src/repository/object.rs index d3adabbe294..873e143792b 100644 --- a/gix/src/repository/object.rs +++ b/gix/src/repository/object.rs @@ -342,7 +342,7 @@ impl crate::Repository { /// Create a new commit object with `message` referring to `tree` with `parents`, and point `reference` /// to it. The commit is written without message encoding field, which can be assumed to be UTF-8. /// `author` and `committer` fields are pre-set from the configuration, which can be altered - /// [temporarily][crate::Repository::config_snapshot_mut()] before the call if required. + /// [temporarily](crate::Repository::config_snapshot_mut()) before the call if required. /// /// `reference` will be created if it doesn't exist, and can be `"HEAD"` to automatically write-through to the symbolic reference /// that `HEAD` points to if it is not detached. For this reason, detached head states cannot be created unless the `HEAD` is detached @@ -352,6 +352,11 @@ impl crate::Repository { /// If there is no parent, the `reference` is expected to not exist yet. /// /// The method fails immediately if a `reference` lock can't be acquired. + /// + /// ### Writing a commit without `reference` update + /// + /// If the reference shouldn't be updated, use [`Self::write_object()`] along with a newly created [`crate::objs::Object`] whose fields + /// can be fully defined. pub fn commit( &self, reference: Name,