1use std::{
15 fmt::Write as _,
16 str::{self, FromStr},
17};
18
19use git2::{ObjectType, Oid};
20use git_trailers::{self as trailers, OwnedTrailer, Trailer};
21
22pub mod author;
23pub use author::Author;
24
25pub mod headers;
26pub use headers::{Headers, Signature};
27
28#[derive(Debug)]
31pub struct Commit {
32 tree: Oid,
33 parents: Vec<Oid>,
34 author: Author,
35 committer: Author,
36 headers: Headers,
37 message: String,
38 trailers: Vec<OwnedTrailer>,
39}
40
41impl Commit {
42 pub fn new<I, T>(
43 tree: Oid,
44 parents: Vec<Oid>,
45 author: Author,
46 committer: Author,
47 headers: Headers,
48 message: String,
49 trailers: I,
50 ) -> Self
51 where
52 I: IntoIterator<Item = T>,
53 OwnedTrailer: From<T>,
54 {
55 let trailers = trailers.into_iter().map(OwnedTrailer::from).collect();
56 Self {
57 tree,
58 parents,
59 author,
60 committer,
61 headers,
62 message,
63 trailers,
64 }
65 }
66
67 pub fn read(repo: &git2::Repository, oid: Oid) -> Result<Self, error::Read> {
70 let odb = repo.odb()?;
71 let object = odb.read(oid)?;
72 Ok(Commit::try_from(object.data())?)
73 }
74
75 pub fn write(&self, repo: &git2::Repository) -> Result<Oid, git2::Error> {
78 let odb = repo.odb()?;
79 odb.write(ObjectType::Commit, self.to_string().as_bytes())
80 }
81
82 pub fn tree(&self) -> Oid {
84 self.tree
85 }
86
87 pub fn parents(&self) -> impl Iterator<Item = Oid> + '_ {
89 self.parents.iter().copied()
90 }
91
92 pub fn author(&self) -> &Author {
94 &self.author
95 }
96
97 pub fn committer(&self) -> &Author {
100 &self.committer
101 }
102
103 pub fn message(&self) -> &str {
105 &self.message
106 }
107
108 pub fn signatures(&self) -> impl Iterator<Item = Signature> + '_ {
111 self.headers.signatures()
112 }
113
114 pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
118 self.headers.iter()
119 }
120
121 pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + '_ {
123 self.headers.values(name)
124 }
125
126 pub fn push_header(&mut self, name: &str, value: &str) {
128 self.headers.push(name, value.trim());
129 }
130
131 pub fn trailers(&self) -> impl Iterator<Item = &OwnedTrailer> {
132 self.trailers.iter()
133 }
134}
135
136pub mod error {
137 use std::str;
138
139 use thiserror::Error;
140
141 use super::author;
142
143 #[derive(Debug, Error)]
144 pub enum Read {
145 #[error(transparent)]
146 Git(#[from] git2::Error),
147 #[error(transparent)]
148 Parse(#[from] Parse),
149 }
150
151 #[derive(Debug, Error)]
152 pub enum Parse {
153 #[error(transparent)]
154 Author(#[from] author::ParseError),
155 #[error("invalid '{header}'")]
156 InvalidHeader {
157 header: &'static str,
158 #[source]
159 err: git2::Error,
160 },
161 #[error("invalid git commit object format")]
162 InvalidFormat,
163 #[error("missing '{0}' while parsing commit")]
164 Missing(&'static str),
165 #[error(transparent)]
166 Token(#[from] git_trailers::InvalidToken),
167 #[error("error occurred while checking for git-trailers: {0}")]
168 Trailers(#[source] git2::Error),
169 #[error(transparent)]
170 Utf8(#[from] str::Utf8Error),
171 }
172}
173
174impl TryFrom<git2::Buf> for Commit {
175 type Error = error::Parse;
176
177 fn try_from(value: git2::Buf) -> Result<Self, Self::Error> {
178 value.as_str().ok_or(error::Parse::InvalidFormat)?.parse()
179 }
180}
181
182impl TryFrom<&[u8]> for Commit {
183 type Error = error::Parse;
184
185 fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
186 Commit::from_str(str::from_utf8(data)?)
187 }
188}
189
190impl FromStr for Commit {
191 type Err = error::Parse;
192
193 fn from_str(buffer: &str) -> Result<Self, Self::Err> {
194 let (header, message) = buffer
195 .split_once("\n\n")
196 .ok_or(error::Parse::InvalidFormat)?;
197 let mut lines = header.lines();
198
199 let tree = match lines.next() {
200 Some(tree) => tree
201 .strip_prefix("tree ")
202 .map(git2::Oid::from_str)
203 .transpose()
204 .map_err(|err| error::Parse::InvalidHeader {
205 header: "tree",
206 err,
207 })?
208 .ok_or(error::Parse::Missing("tree"))?,
209 None => return Err(error::Parse::Missing("tree")),
210 };
211
212 let mut parents = Vec::new();
213 let mut author: Option<Author> = None;
214 let mut committer: Option<Author> = None;
215 let mut headers = Headers::new();
216
217 for line in lines {
218 if let Some(rest) = line.strip_prefix(' ') {
220 let value: &mut String = headers
221 .0
222 .last_mut()
223 .map(|(_, v)| v)
224 .ok_or(error::Parse::InvalidFormat)?;
225 value.push('\n');
226 value.push_str(rest);
227 continue;
228 }
229
230 if let Some((name, value)) = line.split_once(' ') {
231 match name {
232 "parent" => parents.push(git2::Oid::from_str(value).map_err(|err| {
233 error::Parse::InvalidHeader {
234 header: "parent",
235 err,
236 }
237 })?),
238 "author" => author = Some(value.parse::<Author>()?),
239 "committer" => committer = Some(value.parse::<Author>()?),
240 _ => headers.push(name, value),
241 }
242 continue;
243 }
244 }
245
246 let (message, trailers) = message.lines().fold(
247 (Vec::new(), Vec::new()),
248 |(mut message, mut trailers), line| match trailers::parser::trailer(line, ": ") {
249 Ok((_, trailer)) => {
250 trailers.push(trailer.into());
251 (message, trailers)
252 },
253 Err(_) => {
254 message.push(line);
255 (message, trailers)
256 },
257 },
258 );
259
260 Ok(Self {
261 tree,
262 parents,
263 author: author.ok_or(error::Parse::Missing("author"))?,
264 committer: committer.ok_or(error::Parse::Missing("committer"))?,
265 headers,
266 message: message.join("\n"),
267 trailers,
268 })
269 }
270}
271
272impl ToString for Commit {
273 fn to_string(&self) -> String {
274 let mut buf = String::new();
275
276 writeln!(buf, "tree {}", self.tree).ok();
277
278 for parent in &self.parents {
279 writeln!(buf, "parent {parent}").ok();
280 }
281
282 writeln!(buf, "author {}", self.author).ok();
283 writeln!(buf, "committer {}", self.committer).ok();
284
285 for (name, value) in self.headers.iter() {
286 writeln!(buf, "{name} {}", value.replace('\n', "\n ")).ok();
287 }
288 writeln!(buf).ok();
289 write!(buf, "{}", self.message.trim()).ok();
290 writeln!(buf).ok();
291
292 if !self.trailers.is_empty() {
293 writeln!(buf).ok();
294 }
295 for (i, trailer) in self.trailers.iter().enumerate() {
296 if i < self.trailers.len() {
297 writeln!(buf, "{}", Trailer::from(trailer).display(": ")).ok();
298 } else {
299 write!(buf, "{}", Trailer::from(trailer).display(": ")).ok();
300 }
301 }
302 buf
303 }
304}