Skip to main content

gix_object/commit/
mod.rs

1use bstr::{BStr, ByteSlice};
2
3use crate::parse::parse_signature;
4use crate::{Commit, CommitRef, TagRef};
5
6/// The well-known field name for signatures on SHA-1 commits.
7pub const SIGNATURE_FIELD_NAME: &str = "gpgsig";
8/// The well-known field name for signatures on SHA-256 commits.
9pub const SIGNATURE_FIELD_NAME_SHA256: &str = "gpgsig-sha256";
10
11/// Return the signature field name Git uses for `hash_kind`.
12pub fn signature_field_name(hash_kind: gix_hash::Kind) -> &'static str {
13    #[cfg(feature = "sha256")]
14    if hash_kind == gix_hash::Kind::Sha256 {
15        return SIGNATURE_FIELD_NAME_SHA256;
16    }
17    let _ = hash_kind;
18    SIGNATURE_FIELD_NAME
19}
20
21mod decode;
22///
23pub mod message;
24
25/// A parsed commit message that assumes a title separated from the body by two consecutive newlines.
26///
27/// Titles can have any amount of whitespace
28#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct MessageRef<'a> {
31    /// The title of the commit, as separated from the body with two consecutive newlines. The newlines are not included.
32    #[cfg_attr(feature = "serde", serde(borrow))]
33    pub title: &'a BStr,
34    /// All bytes not consumed by the title, excluding the separating newlines.
35    ///
36    /// The body is `None` if there was now title separation or the body was empty after the separator.
37    pub body: Option<&'a BStr>,
38}
39
40///
41pub mod ref_iter;
42
43mod write;
44
45/// Lifecycle
46impl<'a> CommitRef<'a> {
47    /// Deserialize a commit from the given `data` bytes while avoiding most allocations, using `object_hash` to know
48    /// what kind of hash to expect for validation.
49    pub fn from_bytes(mut data: &'a [u8], object_hash: gix_hash::Kind) -> Result<CommitRef<'a>, crate::decode::Error> {
50        let input = &mut data;
51        match decode::commit(input, object_hash) {
52            Ok(tag) => Ok(tag),
53            Err(err) => Err(err),
54        }
55    }
56}
57
58/// Access
59impl<'a> CommitRef<'a> {
60    /// Return the `tree` fields hash digest.
61    pub fn tree(&self) -> gix_hash::ObjectId {
62        gix_hash::ObjectId::from_hex(self.tree).expect("prior validation of tree hash during parsing")
63    }
64
65    /// Returns an iterator of parent object ids
66    pub fn parents(&self) -> impl Iterator<Item = gix_hash::ObjectId> + '_ {
67        self.parents
68            .iter()
69            .map(|hex_hash| gix_hash::ObjectId::from_hex(hex_hash).expect("prior validation of hashes during parsing"))
70    }
71
72    /// Returns a convenient iterator over all extra headers.
73    pub fn extra_headers(&self) -> ExtraHeaders<impl Iterator<Item = (&BStr, &BStr)>> {
74        ExtraHeaders::new(
75            self.extra_headers.iter().map(|(k, v)| (*k, v.as_ref())),
76            self.tree().kind(),
77        )
78    }
79
80    /// Return the author, with whitespace trimmed.
81    ///
82    /// This is different from the `author` field which may contain whitespace.
83    pub fn author(&self) -> Result<gix_actor::SignatureRef<'a>, crate::decode::Error> {
84        parse_signature(self.author).map(|signature| signature.trim())
85    }
86
87    /// Return the committer, with whitespace trimmed.
88    ///
89    /// This is different from the `committer` field which may contain whitespace.
90    pub fn committer(&self) -> Result<gix_actor::SignatureRef<'a>, crate::decode::Error> {
91        parse_signature(self.committer).map(|signature| signature.trim())
92    }
93
94    /// Returns a partially parsed message from which more information can be derived.
95    pub fn message(&self) -> MessageRef<'a> {
96        MessageRef::from_bytes(self.message)
97    }
98
99    /// Returns the time at which this commit was created, or a default time if it could not be parsed.
100    pub fn time(&self) -> Result<gix_date::Time, crate::decode::Error> {
101        parse_signature(self.committer).map(|signature| signature.time().unwrap_or_default())
102    }
103}
104
105/// Conversion
106impl CommitRef<'_> {
107    /// Copy all fields of this instance into a fully owned commit, consuming this instance.
108    pub fn into_owned(self) -> Result<Commit, crate::decode::Error> {
109        self.try_into()
110    }
111
112    /// Copy all fields of this instance into a fully owned commit, internally cloning this instance.
113    pub fn to_owned(self) -> Result<Commit, crate::decode::Error> {
114        self.try_into()
115    }
116}
117
118impl Commit {
119    /// Returns a convenient iterator over all extra headers.
120    pub fn extra_headers(&self) -> ExtraHeaders<impl Iterator<Item = (&BStr, &BStr)>> {
121        ExtraHeaders::new(
122            self.extra_headers.iter().map(|(k, v)| (k.as_bstr(), v.as_bstr())),
123            self.tree.kind(),
124        )
125    }
126}
127
128/// An iterator over extra headers in [owned][crate::Commit] and [borrowed][crate::CommitRef] commits.
129pub struct ExtraHeaders<I> {
130    inner: I,
131    hash_kind: gix_hash::Kind,
132}
133
134/// Instantiation and convenience.
135impl<'a, I> ExtraHeaders<I>
136where
137    I: Iterator<Item = (&'a BStr, &'a BStr)>,
138{
139    /// Create a new instance from an iterator over tuples of (name, value) pairs.
140    pub fn new(iter: I, hash_kind: gix_hash::Kind) -> Self {
141        ExtraHeaders { inner: iter, hash_kind }
142    }
143
144    /// Find the _value_ of the _first_ header with the given `name`.
145    pub fn find(mut self, name: &str) -> Option<&'a BStr> {
146        self.inner
147            .find_map(move |(k, v)| if k == name.as_bytes().as_bstr() { Some(v) } else { None })
148    }
149
150    /// Find the entry index with the given name, or return `None` if unavailable.
151    pub fn find_pos(self, name: &str) -> Option<usize> {
152        self.inner
153            .enumerate()
154            .find_map(|(pos, (field, _value))| (field == name).then_some(pos))
155    }
156
157    /// Return an iterator over all _values_ of headers with the given `name`.
158    pub fn find_all(self, name: &'a str) -> impl Iterator<Item = &'a BStr> {
159        self.inner
160            .filter_map(move |(k, v)| if k == name.as_bytes().as_bstr() { Some(v) } else { None })
161    }
162
163    /// Return an iterator over all git mergetags.
164    ///
165    /// A merge tag is a tag object embedded within the respective header field of a commit, making
166    /// it a child object of sorts.
167    pub fn mergetags(self) -> impl Iterator<Item = Result<TagRef<'a>, crate::decode::Error>> {
168        let hash_kind = self.hash_kind;
169        self.find_all("mergetag").map(move |b| TagRef::from_bytes(b, hash_kind))
170    }
171
172    /// Return the cryptographic signature provided by gpg/pgp verbatim.
173    pub fn pgp_signature(self) -> Option<&'a BStr> {
174        let field_name = signature_field_name(self.hash_kind);
175        self.find(field_name)
176    }
177}