Skip to main content

gix_object/
lib.rs

1//! This crate provides types for [read-only git objects][crate::ObjectRef] backed by bytes provided in git's serialization format
2//! as well as [mutable versions][Object] of these. Both types of objects can be encoded.
3//!
4//! ## Decode Borrowed Objects
5//!
6//! ```
7//! let object = gix_object::ObjectRef::from_loose(b"blob 5\0hello", gix_hash::Kind::Sha1).unwrap();
8//! let blob = object.as_blob().unwrap();
9//!
10//! assert_eq!(blob.data, b"hello");
11//! assert_eq!(object.kind(), gix_object::Kind::Blob);
12//! ```
13//!
14//! ## Mutate And Encode Owned Objects
15//!
16//! ```
17//! use gix_object::WriteTo;
18//!
19//! let object = gix_object::ObjectRef::from_loose(b"blob 5\0hello", gix_hash::Kind::Sha1)
20//!     .unwrap()
21//!     .into_owned()
22//!     .unwrap();
23//! let mut blob = object.into_blob();
24//! blob.data.extend_from_slice(b" world");
25//!
26//! let mut out = Vec::new();
27//! blob.write_to(&mut out).unwrap();
28//! assert_eq!(out, b"hello world");
29//! assert_eq!(blob.loose_header().as_slice(), b"blob 11\0");
30//! ```
31//! ## Feature Flags
32#![cfg_attr(
33    all(doc, feature = "document-features"),
34    doc = ::document_features::document_features!()
35)]
36#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))]
37#![deny(missing_docs)]
38#![forbid(unsafe_code)]
39
40use std::borrow::Cow;
41
42/// For convenience to allow using `bstr` without adding it to own cargo manifest.
43pub use bstr;
44use bstr::{BStr, BString, ByteSlice};
45/// For convenience to allow using `gix-date` without adding it to own cargo manifest.
46pub use gix_date as date;
47use smallvec::SmallVec;
48
49///
50pub mod commit;
51mod object;
52/// Cryptographic signature discovery and, with the `signature` feature, external signing and verification.
53pub mod signature;
54///
55pub mod tag;
56///
57pub mod tree;
58
59mod blob;
60///
61pub mod data;
62
63///
64pub mod find;
65
66///
67pub mod write {
68    /// The error type returned by the [`Write`](crate::Write) trait.
69    pub type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
70}
71
72mod traits;
73pub use traits::{Exists, Find, FindExt, FindObjectOrHeader, Header as FindHeader, HeaderExt, Write, WriteTo};
74
75pub mod encode;
76pub(crate) mod parse;
77
78///
79pub mod kind;
80
81/// The four types of objects that git differentiates.
82#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
83#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
84#[expect(missing_docs)]
85pub enum Kind {
86    Tree,
87    Blob,
88    Commit,
89    Tag,
90}
91/// A chunk of any [`data`](BlobRef::data).
92#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
93#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
94pub struct BlobRef<'a> {
95    /// The bytes themselves.
96    pub data: &'a [u8],
97}
98
99/// A mutable chunk of any [`data`](Blob::data).
100#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
101#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
102pub struct Blob {
103    /// The data itself.
104    pub data: Vec<u8>,
105}
106
107/// A git commit parsed using [`from_bytes()`](CommitRef::from_bytes()).
108///
109/// A commit encapsulates information about a point in time at which the state of the repository is recorded, usually after a
110/// change which is documented in the commit `message`.
111#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
112#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
113pub struct CommitRef<'a> {
114    /// HEX hash of tree object we point to.
115    ///
116    /// Use [`tree()`](CommitRef::tree()) to obtain a decoded version of it.
117    #[cfg_attr(feature = "serde", serde(borrow))]
118    pub tree: &'a BStr,
119    /// HEX hash of each parent commit. Empty for first commit in repository.
120    pub parents: SmallVec<[&'a BStr; 1]>,
121    /// The raw author header value as encountered during parsing.
122    ///
123    /// Use the [`author()`](CommitRef::author()) method to obtain a parsed version of it.
124    #[cfg_attr(feature = "serde", serde(borrow))]
125    pub author: &'a BStr,
126    /// The raw committer header value as encountered during parsing.
127    ///
128    /// Use the [`committer()`](CommitRef::committer()) method to obtain a parsed version of it.
129    #[cfg_attr(feature = "serde", serde(borrow))]
130    pub committer: &'a BStr,
131    /// The name of the message encoding, otherwise [UTF-8 should be assumed](https://github.com/git/git/blob/e67fbf927dfdf13d0b21dc6ea15dc3c7ef448ea0/commit.c#L1493:L1493).
132    pub encoding: Option<&'a BStr>,
133    /// The commit message documenting the change.
134    pub message: &'a BStr,
135    /// Extra header fields, in order of them being encountered, made accessible with the iterator returned by [`extra_headers()`](CommitRef::extra_headers()).
136    pub extra_headers: Vec<(&'a BStr, Cow<'a, BStr>)>,
137}
138
139/// Like [`CommitRef`], but as `Iterator` to support (up to) entirely allocation free parsing.
140/// It's particularly useful to traverse the commit graph without ever allocating arrays for parents.
141#[derive(Copy, Clone)]
142pub struct CommitRefIter<'a> {
143    data: &'a [u8],
144    state: commit::ref_iter::State,
145    hash_kind: gix_hash::Kind,
146}
147
148/// A mutable git commit, representing an annotated state of a working tree along with a reference to its historical commits.
149#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
150#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
151pub struct Commit {
152    /// The hash of recorded working tree state.
153    pub tree: gix_hash::ObjectId,
154    /// Hash of each parent commit. Empty for the first commit in repository.
155    pub parents: SmallVec<[gix_hash::ObjectId; 1]>,
156    /// Who wrote this commit.
157    pub author: gix_actor::Signature,
158    /// Who committed this commit.
159    ///
160    /// This may be different from the `author` in case the author couldn't write to the repository themselves and
161    /// is commonly encountered with contributed commits.
162    pub committer: gix_actor::Signature,
163    /// The name of the message encoding, otherwise [UTF-8 should be assumed](https://github.com/git/git/blob/e67fbf927dfdf13d0b21dc6ea15dc3c7ef448ea0/commit.c#L1493:L1493).
164    pub encoding: Option<BString>,
165    /// The commit message documenting the change.
166    pub message: BString,
167    /// Extra header fields, in order of them being encountered, made accessible with the iterator returned
168    /// by [`extra_headers()`](Commit::extra_headers()).
169    pub extra_headers: Vec<(BString, BString)>,
170}
171
172/// Represents a git tag, commonly indicating a software release.
173#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
174#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
175pub struct TagRef<'a> {
176    /// The hash in hexadecimal being the object this tag points to. Use [`target()`](TagRef::target()) to obtain a byte representation.
177    #[cfg_attr(feature = "serde", serde(borrow))]
178    pub target: &'a BStr,
179    /// The kind of object that `target` points to.
180    pub target_kind: Kind,
181    /// The name of the tag, e.g. "v1.0".
182    pub name: &'a BStr,
183    /// The raw tagger header value as encountered during parsing.
184    ///
185    /// Use the [`tagger()`](TagRef::tagger()) method to obtain a parsed version of it.
186    #[cfg_attr(feature = "serde", serde(borrow))]
187    pub tagger: Option<&'a BStr>,
188    /// The message describing this release.
189    pub message: &'a BStr,
190    /// Any Git-supported in-body cryptographic signature.
191    ///
192    /// Use [`signature()`](TagRef::signature()) to also obtain its detected format.
193    pub signature: Option<&'a BStr>,
194}
195
196/// Like [`TagRef`], but as `Iterator` to support entirely allocation free parsing.
197/// It's particularly useful to dereference only the target chain.
198#[derive(Copy, Clone)]
199pub struct TagRefIter<'a> {
200    data: &'a [u8],
201    state: tag::ref_iter::State,
202    hash_kind: gix_hash::Kind,
203}
204
205/// A mutable git tag.
206#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
207#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
208pub struct Tag {
209    /// The hash this tag is pointing to.
210    pub target: gix_hash::ObjectId,
211    /// The kind of object this tag is pointing to.
212    pub target_kind: Kind,
213    /// The name of the tag, e.g. "v1.0".
214    pub name: BString,
215    /// The tags author.
216    pub tagger: Option<gix_actor::Signature>,
217    /// The message describing the tag.
218    pub message: BString,
219    /// Any Git-supported in-body cryptographic signature.
220    ///
221    /// Use [`signature()`](Tag::signature()) to also obtain its detected format.
222    pub signature: Option<BString>,
223}
224
225/// Immutable objects are read-only structures referencing most data from [a byte slice](ObjectRef::from_bytes()).
226///
227/// Immutable objects are expected to be deserialized from bytes that acts as backing store, and they
228/// cannot be mutated or serialized. Instead, one will [convert](ObjectRef::into_owned()) them into their [`mutable`](Object) counterparts
229/// which support mutation and serialization.
230///
231/// An `ObjectRef` is representing [`Trees`](TreeRef), [`Blobs`](BlobRef), [`Commits`](CommitRef), or [`Tags`](TagRef).
232#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
233#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
234#[expect(missing_docs)]
235pub enum ObjectRef<'a> {
236    #[cfg_attr(feature = "serde", serde(borrow))]
237    Tree(TreeRef<'a>),
238    Blob(BlobRef<'a>),
239    Commit(CommitRef<'a>),
240    Tag(TagRef<'a>),
241}
242
243/// Mutable objects with each field being separately allocated and changeable.
244///
245/// Mutable objects are Commits, Trees, Blobs and Tags that can be changed and serialized.
246///
247/// They either created using object [construction](Object) or by [deserializing existing objects](ObjectRef::from_bytes())
248/// and converting these [into mutable copies](ObjectRef::into_owned()) for adjustments.
249///
250/// An `Object` is representing [`Trees`](Tree), [`Blobs`](Blob), [`Commits`](Commit), or [`Tags`](Tag).
251#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
252#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
253#[expect(missing_docs)]
254pub enum Object {
255    Tree(Tree),
256    Blob(Blob),
257    Commit(Commit),
258    Tag(Tag),
259}
260/// A directory snapshot containing files (blobs), directories (trees) and submodules (commits).
261#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
262#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
263pub struct TreeRef<'a> {
264    /// The directories and files contained in this tree.
265    ///
266    /// Beware that the sort order isn't *quite* by name, so one may bisect only with a [`tree::EntryRef`] to handle ordering correctly.
267    #[cfg_attr(feature = "serde", serde(borrow))]
268    pub entries: Vec<tree::EntryRef<'a>>,
269}
270
271/// A directory snapshot containing files (blobs), directories (trees) and submodules (commits), lazily evaluated.
272#[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
273pub struct TreeRefIter<'a> {
274    /// The hash kind to use for parsing this tree.
275    hash_kind: gix_hash::Kind,
276    /// The directories and files contained in this tree.
277    data: &'a [u8],
278}
279
280/// A mutable Tree, containing other trees, blobs or commits.
281#[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
282#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
283pub struct Tree {
284    /// The directories and files contained in this tree. They must be and remain sorted by [`filename`][tree::Entry::filename].
285    ///
286    /// Beware that the sort order isn't *quite* by name, so one may bisect only with a [`tree::Entry`] to handle ordering correctly.
287    pub entries: Vec<tree::Entry>,
288}
289
290impl Tree {
291    /// Return an empty tree which serializes to a well-known hash
292    pub fn empty() -> Self {
293        Tree { entries: Vec::new() }
294    }
295}
296
297/// A borrowed object using a slice as backing buffer, or in other words a bytes buffer that knows the kind of object it represents.
298#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
299pub struct Data<'a> {
300    /// kind of object
301    pub kind: Kind,
302    /// The hash kind to use for parsing this data.
303    pub object_hash: gix_hash::Kind,
304    /// decoded, decompressed data, owned by a backing store.
305    pub data: &'a [u8],
306}
307
308/// Information about an object, which includes its kind and the amount of bytes it would have when obtained.
309#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
310pub struct Header {
311    /// The kind of object.
312    pub kind: Kind,
313    /// The object's size in bytes, or the size of the buffer when it's retrieved in full.
314    pub size: u64,
315}
316
317///
318pub mod decode {
319    mod error {
320        pub(crate) fn empty_error() -> Error {
321            Error
322        }
323
324        /// A type to indicate any error occurred during parsing.
325        #[derive(Debug, Clone, Copy, Default)]
326        pub struct Error;
327
328        impl std::fmt::Display for Error {
329            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330                f.write_str("object parsing failed")
331            }
332        }
333
334        impl std::error::Error for Error {}
335    }
336    pub use error::Error;
337    pub(crate) use error::empty_error;
338
339    /// Returned by [`loose_header()`]
340    #[derive(Debug, thiserror::Error)]
341    #[expect(missing_docs)]
342    pub enum LooseHeaderDecodeError {
343        #[error("{message}: {number:?}")]
344        ParseIntegerError {
345            source: gix_utils::btoi::ParseIntegerError,
346            message: &'static str,
347            number: bstr::BString,
348        },
349        #[error("{message}")]
350        InvalidHeader { message: &'static str },
351        #[error("The object header contained an unknown object kind.")]
352        ObjectHeader(#[from] super::kind::Error),
353    }
354
355    use bstr::ByteSlice;
356    /// Decode a loose object header, being `<kind> <size>\0`, returns
357    /// ([`kind`](super::Kind), `size`, `consumed bytes`).
358    ///
359    /// `size` is the uncompressed size of the payload in bytes.
360    pub fn loose_header(input: &[u8]) -> Result<(super::Kind, u64, usize), LooseHeaderDecodeError> {
361        use LooseHeaderDecodeError::*;
362        let kind_end = input.find_byte(0x20).ok_or(InvalidHeader {
363            message: "Expected '<type> <size>'",
364        })?;
365        let kind = super::Kind::from_bytes(&input[..kind_end])?;
366        let size_end = input.find_byte(0x0).ok_or(InvalidHeader {
367            message: "Did not find 0 byte in header",
368        })?;
369        let size_bytes = &input[kind_end + 1..size_end];
370        let size = gix_utils::btoi::to_signed(size_bytes).map_err(|source| ParseIntegerError {
371            source,
372            message: "Object size in header could not be parsed",
373            number: size_bytes.into(),
374        })?;
375        Ok((kind, size, size_end + 1))
376    }
377}
378
379fn object_hasher(hash_kind: gix_hash::Kind, object_kind: Kind, object_size: u64) -> gix_hash::Hasher {
380    let mut hasher = gix_hash::hasher(hash_kind);
381    hasher.update(&encode::loose_header(object_kind, object_size));
382    hasher
383}
384
385/// A function to compute a hash of kind `object_hash` for an object of `object_kind` and its `data`.
386#[doc(alias = "hash_object", alias = "git2")]
387pub fn compute_hash(
388    hash_kind: gix_hash::Kind,
389    object_kind: Kind,
390    data: &[u8],
391) -> Result<gix_hash::ObjectId, gix_hash::hasher::Error> {
392    let mut hasher = object_hasher(hash_kind, object_kind, data.len() as u64);
393    hasher.update(data);
394    hasher.try_finalize()
395}
396
397/// A function to compute a hash of kind `object_hash` for an object of `object_kind` and its data read from `stream`
398/// which has to yield exactly `stream_len` bytes.
399/// Use `progress` to learn about progress in bytes processed and `should_interrupt` to be able to abort the operation
400/// if set to `true`.
401#[doc(alias = "hash_file", alias = "git2")]
402pub fn compute_stream_hash(
403    hash_kind: gix_hash::Kind,
404    object_kind: Kind,
405    stream: &mut dyn std::io::Read,
406    stream_len: u64,
407    progress: &mut dyn gix_features::progress::Progress,
408    should_interrupt: &std::sync::atomic::AtomicBool,
409) -> Result<gix_hash::ObjectId, gix_hash::io::Error> {
410    let hasher = object_hasher(hash_kind, object_kind, stream_len);
411    gix_hash::bytes_with_hasher(stream, stream_len, hasher, progress, should_interrupt)
412}