Skip to main content

radicle_git_metadata/
commit.rs

1pub mod headers;
2pub mod trailers;
3
4mod parse;
5pub use parse::ParseError;
6
7use core::fmt;
8use std::str::{self, FromStr};
9
10use headers::{Headers, Signature};
11use trailers::{OwnedTrailer, Trailer};
12
13use crate::author::Author;
14
15/// A git commit in its object description form, i.e. the output of
16/// `git cat-file` for a commit object.
17#[derive(Clone, Debug, PartialEq, Eq, Hash)]
18pub struct CommitData<Tree, Parent> {
19    tree: Tree,
20    parents: Vec<Parent>,
21    author: Author,
22    committer: Author,
23    headers: Headers,
24    message: String,
25    trailers: Vec<OwnedTrailer>,
26}
27
28impl<Tree, Parent> CommitData<Tree, Parent> {
29    pub fn new<P, I, T>(
30        tree: Tree,
31        parents: P,
32        author: Author,
33        committer: Author,
34        headers: Headers,
35        message: String,
36        trailers: I,
37    ) -> Self
38    where
39        P: IntoIterator<Item = Parent>,
40        I: IntoIterator<Item = T>,
41        OwnedTrailer: From<T>,
42    {
43        let trailers = trailers.into_iter().map(OwnedTrailer::from).collect();
44        let parents = parents.into_iter().collect();
45        Self {
46            tree,
47            parents,
48            author,
49            committer,
50            headers,
51            message,
52            trailers,
53        }
54    }
55
56    /// The tree this commit points to.
57    pub fn tree(&self) -> &Tree {
58        &self.tree
59    }
60
61    /// The parents of this commit.
62    pub fn parents(&self) -> impl Iterator<Item = Parent> + '_
63    where
64        Parent: Clone,
65    {
66        self.parents.iter().cloned()
67    }
68
69    /// The author of this commit, i.e. the header corresponding to `author`.
70    pub fn author(&self) -> &Author {
71        &self.author
72    }
73
74    /// The committer of this commit, i.e. the header corresponding to
75    /// `committer`.
76    pub fn committer(&self) -> &Author {
77        &self.committer
78    }
79
80    /// The message body of this commit.
81    pub fn message(&self) -> &str {
82        &self.message
83    }
84
85    /// The [`Signature`]s found in this commit, i.e. the headers corresponding
86    /// to `gpgsig`.
87    pub fn signatures(&self) -> impl Iterator<Item = Signature<'_>> + '_ {
88        self.headers.signatures()
89    }
90
91    pub fn strip_signatures(mut self) -> Self {
92        self.headers.strip_signatures();
93        self
94    }
95
96    /// The [`Headers`] found in this commit.
97    ///
98    /// Note: these do not include `tree`, `parent`, `author`, and `committer`.
99    pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
100        self.headers.iter()
101    }
102
103    /// Iterate over the [`Headers`] values that match the provided `name`.
104    pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
105        self.headers.values(name)
106    }
107
108    /// Push a header to the end of the headers section.
109    pub fn push_header(&mut self, name: &str, value: &str) {
110        self.headers.push(name, value.trim());
111    }
112
113    pub fn trailers(&self) -> impl Iterator<Item = &OwnedTrailer> {
114        self.trailers.iter()
115    }
116
117    /// Convert the `CommitData::tree` into a value of type `U`. The
118    /// conversion function `f` can be fallible.
119    ///
120    /// For example, `map_tree` can be used to turn raw tree data into
121    /// an `Oid` by writing it to a repository.
122    pub fn map_tree<U, E, F>(self, f: F) -> Result<CommitData<U, Parent>, E>
123    where
124        F: FnOnce(Tree) -> Result<U, E>,
125    {
126        Ok(CommitData {
127            tree: f(self.tree)?,
128            parents: self.parents,
129            author: self.author,
130            committer: self.committer,
131            headers: self.headers,
132            message: self.message,
133            trailers: self.trailers,
134        })
135    }
136
137    /// Convert the [`CommitData::parents`] into a vector containing
138    /// values of type `U`. The conversion function `f` can be
139    /// fallible.
140    ///
141    /// For example, this can be used to resolve the object identifiers
142    /// to their respective full commits.
143    pub fn map_parents<U, E, F>(self, f: F) -> Result<CommitData<Tree, U>, E>
144    where
145        F: FnMut(Parent) -> Result<U, E>,
146    {
147        Ok(CommitData {
148            tree: self.tree,
149            parents: self
150                .parents
151                .into_iter()
152                .map(f)
153                .collect::<Result<Vec<_>, _>>()?,
154            author: self.author,
155            committer: self.committer,
156            headers: self.headers,
157            message: self.message,
158            trailers: self.trailers,
159        })
160    }
161}
162
163impl<Tree, Parent> CommitData<Tree, Parent>
164where
165    Tree: str::FromStr,
166    Parent: str::FromStr,
167    Tree::Err: std::error::Error + Send + Sync + 'static,
168    Parent::Err: std::error::Error + Send + Sync + 'static,
169{
170    /// Parse a [`CommitData`] from its raw git object bytes.
171    ///
172    /// This is the inverse of the [`fmt::Display`] implementation. The bytes
173    /// are expected to be valid UTF-8 and in the standard git commit object
174    /// format produced by `git cat-file -p <commit>`.
175    ///
176    /// Trailers are detected by scanning the last paragraph of the message
177    /// body (the section after the final blank line). If every non-empty line
178    /// in that paragraph is a valid `Token: value` pair, those lines are
179    /// parsed as trailers and stored separately; otherwise the whole body is
180    /// kept as the message with no trailers.
181    pub fn from_bytes(bytes: &[u8]) -> Result<Self, ParseError> {
182        let s = str::from_utf8(bytes).map_err(ParseError::Utf8)?;
183        parse::parse(s)
184    }
185}
186
187impl<Tree, Parent> FromStr for CommitData<Tree, Parent>
188where
189    Tree: str::FromStr,
190    Parent: str::FromStr,
191    Tree::Err: std::error::Error + Send + Sync + 'static,
192    Parent::Err: std::error::Error + Send + Sync + 'static,
193{
194    type Err = ParseError;
195
196    fn from_str(s: &str) -> Result<Self, Self::Err> {
197        parse::parse(s)
198    }
199}
200
201impl<Tree: fmt::Display, Parent: fmt::Display> fmt::Display for CommitData<Tree, Parent> {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        writeln!(f, "tree {}", self.tree)?;
204        for parent in self.parents.iter() {
205            writeln!(f, "parent {parent}")?;
206        }
207        writeln!(f, "author {}", self.author)?;
208        writeln!(f, "committer {}", self.committer)?;
209
210        for (name, value) in self.headers.iter() {
211            writeln!(f, "{name} {}", value.replace('\n', "\n "))?;
212        }
213        writeln!(f)?;
214        write!(f, "{}", self.message.trim())?;
215        writeln!(f)?;
216
217        if !self.trailers.is_empty() {
218            writeln!(f)?;
219        }
220        for trailer in self.trailers.iter() {
221            writeln!(f, "{}", Trailer::from(trailer).display(": "))?;
222        }
223        Ok(())
224    }
225}