1pub mod headers;
15pub mod trailers;
16
17use core::fmt;
18use std::str::{self, FromStr};
19
20use git2::{ObjectType, Oid};
21
22use headers::{Headers, Signature};
23use trailers::{OwnedTrailer, Trailer, Trailers};
24
25use crate::author::Author;
26
27pub type Commit = CommitData<Oid, Oid>;
28
29impl Commit {
30 pub fn read(repo: &git2::Repository, oid: Oid) -> Result<Self, error::Read> {
33 let odb = repo.odb()?;
34 let object = odb.read(oid)?;
35 Ok(Commit::try_from(object.data())?)
36 }
37
38 pub fn write(&self, repo: &git2::Repository) -> Result<Oid, error::Write> {
41 let odb = repo.odb().map_err(error::Write::Odb)?;
42 self.verify_for_write(&odb)?;
43 Ok(odb.write(ObjectType::Commit, self.to_string().as_bytes())?)
44 }
45
46 fn verify_for_write(&self, odb: &git2::Odb) -> Result<(), error::Write> {
47 for parent in &self.parents {
48 verify_object(odb, parent, ObjectType::Commit)?;
49 }
50 verify_object(odb, &self.tree, ObjectType::Tree)?;
51
52 Ok(())
53 }
54}
55
56#[derive(Debug)]
59pub struct CommitData<Tree, Parent> {
60 tree: Tree,
61 parents: Vec<Parent>,
62 author: Author,
63 committer: Author,
64 headers: Headers,
65 message: String,
66 trailers: Vec<OwnedTrailer>,
67}
68
69impl<Tree, Parent> CommitData<Tree, Parent> {
70 pub fn new<P, I, T>(
71 tree: Tree,
72 parents: P,
73 author: Author,
74 committer: Author,
75 headers: Headers,
76 message: String,
77 trailers: I,
78 ) -> Self
79 where
80 P: IntoIterator<Item = Parent>,
81 I: IntoIterator<Item = T>,
82 OwnedTrailer: From<T>,
83 {
84 let trailers = trailers.into_iter().map(OwnedTrailer::from).collect();
85 let parents = parents.into_iter().collect();
86 Self {
87 tree,
88 parents,
89 author,
90 committer,
91 headers,
92 message,
93 trailers,
94 }
95 }
96
97 pub fn tree(&self) -> &Tree {
99 &self.tree
100 }
101
102 pub fn parents(&self) -> impl Iterator<Item = Parent> + '_
104 where
105 Parent: Clone,
106 {
107 self.parents.iter().cloned()
108 }
109
110 pub fn author(&self) -> &Author {
112 &self.author
113 }
114
115 pub fn committer(&self) -> &Author {
118 &self.committer
119 }
120
121 pub fn message(&self) -> &str {
123 &self.message
124 }
125
126 pub fn signatures(&self) -> impl Iterator<Item = Signature> + '_ {
129 self.headers.signatures()
130 }
131
132 pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
136 self.headers.iter()
137 }
138
139 pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
141 self.headers.values(name)
142 }
143
144 pub fn push_header(&mut self, name: &str, value: &str) {
146 self.headers.push(name, value.trim());
147 }
148
149 pub fn trailers(&self) -> impl Iterator<Item = &OwnedTrailer> {
150 self.trailers.iter()
151 }
152
153 pub fn map_tree<U, E, F>(self, f: F) -> Result<CommitData<U, Parent>, E>
159 where
160 F: FnOnce(Tree) -> Result<U, E>,
161 {
162 Ok(CommitData {
163 tree: f(self.tree)?,
164 parents: self.parents,
165 author: self.author,
166 committer: self.committer,
167 headers: self.headers,
168 message: self.message,
169 trailers: self.trailers,
170 })
171 }
172
173 pub fn map_parents<U, E, F>(self, f: F) -> Result<CommitData<Tree, U>, E>
180 where
181 F: FnMut(Parent) -> Result<U, E>,
182 {
183 Ok(CommitData {
184 tree: self.tree,
185 parents: self
186 .parents
187 .into_iter()
188 .map(f)
189 .collect::<Result<Vec<_>, _>>()?,
190 author: self.author,
191 committer: self.committer,
192 headers: self.headers,
193 message: self.message,
194 trailers: self.trailers,
195 })
196 }
197}
198
199fn verify_object(odb: &git2::Odb, oid: &Oid, expected: ObjectType) -> Result<(), error::Write> {
200 use git2::{Error, ErrorClass, ErrorCode};
201
202 let (_, kind) = odb
203 .read_header(*oid)
204 .map_err(|err| error::Write::OdbRead { oid: *oid, err })?;
205 if kind != expected {
206 Err(error::Write::NotCommit {
207 oid: *oid,
208 err: Error::new(
209 ErrorCode::NotFound,
210 ErrorClass::Object,
211 format!("Object '{oid}' is not expected object type {expected}"),
212 ),
213 })
214 } else {
215 Ok(())
216 }
217}
218
219pub mod error {
220 use std::str;
221
222 use thiserror::Error;
223
224 use crate::author;
225
226 #[derive(Debug, Error)]
227 pub enum Write {
228 #[error(transparent)]
229 Git(#[from] git2::Error),
230 #[error("the parent '{oid}' provided is not a commit object")]
231 NotCommit {
232 oid: git2::Oid,
233 #[source]
234 err: git2::Error,
235 },
236 #[error("failed to access git odb")]
237 Odb(#[source] git2::Error),
238 #[error("failed to read '{oid}' from git odb")]
239 OdbRead {
240 oid: git2::Oid,
241 #[source]
242 err: git2::Error,
243 },
244 }
245
246 #[derive(Debug, Error)]
247 pub enum Read {
248 #[error(transparent)]
249 Git(#[from] git2::Error),
250 #[error(transparent)]
251 Parse(#[from] Parse),
252 }
253
254 #[derive(Debug, Error)]
255 pub enum Parse {
256 #[error(transparent)]
257 Author(#[from] author::ParseError),
258 #[error("invalid '{header}'")]
259 InvalidHeader {
260 header: &'static str,
261 #[source]
262 err: git2::Error,
263 },
264 #[error("invalid git commit object format")]
265 InvalidFormat,
266 #[error("missing '{0}' while parsing commit")]
267 Missing(&'static str),
268 #[error("error occurred while checking for git-trailers: {0}")]
269 Trailers(#[source] git2::Error),
270 #[error(transparent)]
271 Utf8(#[from] str::Utf8Error),
272 }
273}
274
275impl TryFrom<git2::Buf> for Commit {
276 type Error = error::Parse;
277
278 fn try_from(value: git2::Buf) -> Result<Self, Self::Error> {
279 value.as_str().ok_or(error::Parse::InvalidFormat)?.parse()
280 }
281}
282
283impl TryFrom<&[u8]> for Commit {
284 type Error = error::Parse;
285
286 fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
287 Commit::from_str(str::from_utf8(data)?)
288 }
289}
290
291impl FromStr for Commit {
292 type Err = error::Parse;
293
294 fn from_str(buffer: &str) -> Result<Self, Self::Err> {
295 let (header, message) = buffer
296 .split_once("\n\n")
297 .ok_or(error::Parse::InvalidFormat)?;
298 let mut lines = header.lines();
299
300 let tree = match lines.next() {
301 Some(tree) => tree
302 .strip_prefix("tree ")
303 .map(git2::Oid::from_str)
304 .transpose()
305 .map_err(|err| error::Parse::InvalidHeader {
306 header: "tree",
307 err,
308 })?
309 .ok_or(error::Parse::Missing("tree"))?,
310 None => return Err(error::Parse::Missing("tree")),
311 };
312
313 let mut parents = Vec::new();
314 let mut author: Option<Author> = None;
315 let mut committer: Option<Author> = None;
316 let mut headers = Headers::new();
317
318 for line in lines {
319 if let Some(rest) = line.strip_prefix(' ') {
321 let value: &mut String = headers
322 .0
323 .last_mut()
324 .map(|(_, v)| v)
325 .ok_or(error::Parse::InvalidFormat)?;
326 value.push('\n');
327 value.push_str(rest);
328 continue;
329 }
330
331 if let Some((name, value)) = line.split_once(' ') {
332 match name {
333 "parent" => parents.push(git2::Oid::from_str(value).map_err(|err| {
334 error::Parse::InvalidHeader {
335 header: "parent",
336 err,
337 }
338 })?),
339 "author" => author = Some(value.parse::<Author>()?),
340 "committer" => committer = Some(value.parse::<Author>()?),
341 _ => headers.push(name, value),
342 }
343 continue;
344 }
345 }
346
347 let trailers = Trailers::parse(message).map_err(error::Parse::Trailers)?;
348
349 let message = message
350 .strip_suffix(&trailers.to_string(": "))
351 .unwrap_or(message)
352 .to_string();
353
354 let trailers = trailers.iter().map(OwnedTrailer::from).collect();
355
356 Ok(Self {
357 tree,
358 parents,
359 author: author.ok_or(error::Parse::Missing("author"))?,
360 committer: committer.ok_or(error::Parse::Missing("committer"))?,
361 headers,
362 message,
363 trailers,
364 })
365 }
366}
367
368impl fmt::Display for Commit {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 writeln!(f, "tree {}", self.tree)?;
371 for parent in self.parents() {
372 writeln!(f, "parent {parent}")?;
373 }
374 writeln!(f, "author {}", self.author)?;
375 writeln!(f, "committer {}", self.committer)?;
376
377 for (name, value) in self.headers.iter() {
378 writeln!(f, "{name} {}", value.replace('\n', "\n "))?;
379 }
380 writeln!(f)?;
381 write!(f, "{}", self.message.trim())?;
382 writeln!(f)?;
383
384 if !self.trailers.is_empty() {
385 writeln!(f)?;
386 }
387 for trailer in self.trailers.iter() {
388 writeln!(f, "{}", Trailer::from(trailer).display(": "))?;
389 }
390 Ok(())
391 }
392}