mit_commit/trailers.rs
1use std::{convert::TryFrom, slice::Iter};
2
3use crate::{body::Body, fragment::Fragment, trailer::Trailer};
4
5/// A Collection of `Trailer`
6#[derive(Debug, PartialEq, Eq, Clone, Default)]
7pub struct Trailers<'a> {
8 trailers: Vec<Trailer<'a>>,
9}
10
11impl Trailers<'_> {
12 /// Iterate over the [`Trailers`]
13 ///
14 /// # Examples
15 ///
16 /// ```
17 /// use mit_commit::{Trailer, Trailers};
18 /// let trailers = Trailers::from(vec![
19 /// Trailer::new(
20 /// "Co-authored-by".into(),
21 /// "Billie Thompson <billie@example.com>".into(),
22 /// ),
23 /// Trailer::new(
24 /// "Co-authored-by".into(),
25 /// "Someone Else <someone@example.com>".into(),
26 /// ),
27 /// Trailer::new("Relates-to".into(), "#124".into()),
28 /// ]);
29 /// let mut iterator = trailers.iter();
30 ///
31 /// assert_eq!(
32 /// iterator.next(),
33 /// Some(&Trailer::new(
34 /// "Co-authored-by".into(),
35 /// "Billie Thompson <billie@example.com>".into()
36 /// ))
37 /// );
38 /// assert_eq!(
39 /// iterator.next(),
40 /// Some(&Trailer::new(
41 /// "Co-authored-by".into(),
42 /// "Someone Else <someone@example.com>".into()
43 /// ))
44 /// );
45 /// assert_eq!(
46 /// iterator.next(),
47 /// Some(&Trailer::new("Relates-to".into(), "#124".into()))
48 /// );
49 /// assert_eq!(iterator.next(), None);
50 /// ```
51 pub fn iter(&self) -> Iter<'_, Trailer<'_>> {
52 self.trailers.iter()
53 }
54
55 /// How many [`Trailers`] are there
56 ///
57 /// # Examples
58 ///
59 /// ```
60 /// use mit_commit::{Trailer, Trailers};
61 /// let trailers = Trailers::from(vec![
62 /// Trailer::new(
63 /// "Co-authored-by".into(),
64 /// "Billie Thompson <billie@example.com>".into(),
65 /// ),
66 /// Trailer::new(
67 /// "Co-authored-by".into(),
68 /// "Someone Else <someone@example.com>".into(),
69 /// ),
70 /// ]);
71 ///
72 /// assert_eq!(trailers.len(), 2)
73 /// ```
74 #[must_use]
75 pub const fn len(&self) -> usize {
76 self.trailers.len()
77 }
78
79 /// Are there no [`Trailers`]
80 ///
81 /// # Examples
82 ///
83 /// ```
84 /// use mit_commit::{Trailer, Trailers};
85 /// assert_eq!(
86 /// Trailers::from(vec![
87 /// Trailer::new(
88 /// "Co-authored-by".into(),
89 /// "Billie Thompson <billie@example.com>".into()
90 /// ),
91 /// Trailer::new(
92 /// "Co-authored-by".into(),
93 /// "Someone Else <someone@example.com>".into()
94 /// ),
95 /// ])
96 /// .is_empty(),
97 /// false
98 /// );
99 ///
100 /// let trailers: Vec<Trailer> = Vec::new();
101 /// assert_eq!(Trailers::from(trailers).is_empty(), true)
102 /// ```
103 #[must_use]
104 pub const fn is_empty(&self) -> bool {
105 self.trailers.is_empty()
106 }
107}
108
109impl<'a> IntoIterator for Trailers<'a> {
110 type Item = Trailer<'a>;
111 type IntoIter = std::vec::IntoIter<Trailer<'a>>;
112
113 /// Iterate over the [`Trailers`]
114 ///
115 /// # Examples
116 ///
117 /// ```
118 /// use mit_commit::{Trailer, Trailers};
119 /// let trailers = Trailers::from(vec![
120 /// Trailer::new(
121 /// "Co-authored-by".into(),
122 /// "Billie Thompson <billie@example.com>".into(),
123 /// ),
124 /// Trailer::new(
125 /// "Co-authored-by".into(),
126 /// "Someone Else <someone@example.com>".into(),
127 /// ),
128 /// Trailer::new("Relates-to".into(), "#124".into()),
129 /// ]);
130 /// let mut iterator = trailers.into_iter();
131 ///
132 /// assert_eq!(
133 /// iterator.next(),
134 /// Some(Trailer::new(
135 /// "Co-authored-by".into(),
136 /// "Billie Thompson <billie@example.com>".into()
137 /// ))
138 /// );
139 /// assert_eq!(
140 /// iterator.next(),
141 /// Some(Trailer::new(
142 /// "Co-authored-by".into(),
143 /// "Someone Else <someone@example.com>".into()
144 /// ))
145 /// );
146 /// assert_eq!(
147 /// iterator.next(),
148 /// Some(Trailer::new("Relates-to".into(), "#124".into()))
149 /// );
150 /// assert_eq!(iterator.next(), None);
151 /// ```
152 fn into_iter(self) -> Self::IntoIter {
153 self.trailers.into_iter()
154 }
155}
156impl<'a> IntoIterator for &'a Trailers<'a> {
157 type IntoIter = Iter<'a, Trailer<'a>>;
158 type Item = &'a Trailer<'a>;
159
160 /// Iterate over the [`Trailers`]
161 ///
162 /// # Examples
163 ///
164 /// ```
165 /// use std::borrow::Borrow;
166 ///
167 /// use mit_commit::{Trailer, Trailers};
168 /// let trailers = Trailers::from(vec![
169 /// Trailer::new(
170 /// "Co-authored-by".into(),
171 /// "Billie Thompson <billie@example.com>".into(),
172 /// ),
173 /// Trailer::new(
174 /// "Co-authored-by".into(),
175 /// "Someone Else <someone@example.com>".into(),
176 /// ),
177 /// Trailer::new("Relates-to".into(), "#124".into()),
178 /// ]);
179 /// let trailer_ref = trailers.borrow();
180 /// let mut iterator = trailer_ref.into_iter();
181 ///
182 /// assert_eq!(
183 /// iterator.next(),
184 /// Some(&Trailer::new(
185 /// "Co-authored-by".into(),
186 /// "Billie Thompson <billie@example.com>".into()
187 /// ))
188 /// );
189 /// assert_eq!(
190 /// iterator.next(),
191 /// Some(&Trailer::new(
192 /// "Co-authored-by".into(),
193 /// "Someone Else <someone@example.com>".into()
194 /// ))
195 /// );
196 /// assert_eq!(
197 /// iterator.next(),
198 /// Some(&Trailer::new("Relates-to".into(), "#124".into()))
199 /// );
200 /// assert_eq!(iterator.next(), None);
201 /// ```
202 fn into_iter(self) -> Self::IntoIter {
203 self.trailers.iter()
204 }
205}
206
207impl<'a> From<Vec<Trailer<'a>>> for Trailers<'a> {
208 fn from(trailers: Vec<Trailer<'a>>) -> Self {
209 Self { trailers }
210 }
211}
212
213impl<'a> From<Trailers<'a>> for String {
214 fn from(trailers: Trailers<'a>) -> Self {
215 trailers
216 .trailers
217 .into_iter()
218 .map(Self::from)
219 .collect::<Vec<_>>()
220 .join("\n")
221 }
222}
223
224impl<'a> From<Vec<Fragment<'a>>> for Trailers<'a> {
225 fn from(ast: Vec<Fragment<'a>>) -> Self {
226 let mut bodies: Vec<Body<'a>> = ast
227 .into_iter()
228 .filter_map(|values| {
229 if let Fragment::Body(body) = values {
230 Some(body)
231 } else {
232 None
233 }
234 })
235 .collect();
236
237 // Remove the subject line (first Body fragment) so conventional
238 // commit subjects like "feat: add login" are not mistaken for trailers.
239 if bodies.is_empty() {
240 return Self::default();
241 }
242 bodies.remove(0);
243
244 bodies
245 .into_iter()
246 .rev()
247 .filter_map(|body| {
248 if body.is_empty() {
249 None
250 } else {
251 Some(Trailer::try_from(body))
252 }
253 })
254 .take_while(Result::is_ok)
255 .flatten()
256 .collect::<Vec<Trailer<'_>>>()
257 .into_iter()
258 .rev()
259 .collect::<Vec<Trailer<'_>>>()
260 .into()
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use indoc::indoc;
267
268 use super::Trailers;
269 use crate::{Body, Comment, fragment::Fragment, trailer::Trailer};
270
271 #[test]
272 fn implements_iterator() {
273 let trailers = Trailers::from(vec![
274 Trailer::new(
275 "Co-authored-by".into(),
276 "Billie Thompson <billie@example.com>".into(),
277 ),
278 Trailer::new(
279 "Co-authored-by".into(),
280 "Someone Else <someone@example.com>".into(),
281 ),
282 Trailer::new("Relates-to".into(), "#124".into()),
283 ]);
284 let mut iterator = trailers.iter();
285
286 assert_eq!(
287 iterator.next(),
288 Some(&Trailer::new(
289 "Co-authored-by".into(),
290 "Billie Thompson <billie@example.com>".into(),
291 ))
292 );
293 assert_eq!(
294 iterator.next(),
295 Some(&Trailer::new(
296 "Co-authored-by".into(),
297 "Someone Else <someone@example.com>".into(),
298 ))
299 );
300 assert_eq!(
301 iterator.next(),
302 Some(&Trailer::new("Relates-to".into(), "#124".into()))
303 );
304 assert_eq!(iterator.next(), None);
305 }
306
307 #[test]
308 fn it_can_give_me_it_as_a_string() {
309 let trailers = Trailers::from(vec![Trailer::new(
310 "Co-authored-by".into(),
311 "Billie Thompson <billie@example.com>".into(),
312 )]);
313
314 assert_eq!(
315 String::from(trailers),
316 String::from("Co-authored-by: Billie Thompson <billie@example.com>")
317 );
318 }
319
320 #[test]
321 fn it_can_give_me_the_length() {
322 let trailers = Trailers::from(vec![
323 Trailer::new(
324 "Co-authored-by".into(),
325 "Billie Thompson <billie@example.com>".into(),
326 ),
327 Trailer::new(
328 "Co-authored-by".into(),
329 "Someone Else <someone@example.com>".into(),
330 ),
331 ]);
332
333 assert_eq!(trailers.len(), 2);
334 }
335
336 #[test]
337 fn it_can_tell_me_if_it_is_empty() {
338 assert!(
339 !Trailers::from(vec![
340 Trailer::new(
341 "Co-authored-by".into(),
342 "Billie Thompson <billie@example.com>".into()
343 ),
344 Trailer::new(
345 "Co-authored-by".into(),
346 "Someone Else <someone@example.com>".into()
347 ),
348 ])
349 .is_empty()
350 );
351
352 let trailers: Vec<Trailer<'_>> = Vec::new();
353 assert!(Trailers::from(trailers).is_empty());
354 }
355
356 #[test]
357 fn it_can_be_constructed_from_ast() {
358 let trailers = vec![
359 Fragment::Body(Body::from("Example Commit")),
360 Fragment::Body(Body::default()),
361 Fragment::Body(Body::from(indoc!(
362 "
363 This is an example commit. This is to illustrate something for a test and would be
364 pretty unusual to find in an actual git history.
365 "
366 ))),
367 Fragment::Body(Body::default()),
368 Fragment::Body(Body::from(
369 "Co-authored-by: Billie Thompson <billie@example.com>",
370 )),
371 Fragment::Body(Body::from(
372 "Co-authored-by: Somebody Else <somebody@example.com>",
373 )),
374 ];
375
376 let expected: Trailers<'_> = vec![
377 Trailer::new(
378 "Co-authored-by".into(),
379 "Billie Thompson <billie@example.com>".into(),
380 ),
381 Trailer::new(
382 "Co-authored-by".into(),
383 "Somebody Else <somebody@example.com>".into(),
384 ),
385 ]
386 .into();
387
388 assert_eq!(Trailers::from(trailers), expected);
389 }
390
391 #[test]
392 fn conventional_commit_subject_not_mistaken_for_trailer_when_ast_starts_with_comment() {
393 // When the AST starts with a Comment (e.g. a commit whose first line is
394 // a comment), the subject Body "feat: add login" must NOT be treated as
395 // a trailer. The skip(1) should drop the first Body (the subject), not
396 // the first Fragment (which might be a Comment).
397 let ast = vec![
398 Fragment::Comment(Comment::from("# Comment")),
399 Fragment::Body(Body::from("feat: add login")),
400 Fragment::Body(Body::default()),
401 Fragment::Body(Body::from(
402 "Co-authored-by: Somebody <somebody@example.com>",
403 )),
404 ];
405
406 let expected: Trailers<'_> = vec![Trailer::new(
407 "Co-authored-by".into(),
408 "Somebody <somebody@example.com>".into(),
409 )]
410 .into();
411
412 assert_eq!(
413 Trailers::from(ast),
414 expected,
415 "Conventional commit subject should not be parsed as a trailer"
416 );
417 }
418
419 #[test]
420 fn it_can_be_constructed_from_ast_with_conventional_commits() {
421 let trailers = vec![
422 Fragment::Body(Body::from("feat: Example Commit")),
423 Fragment::Body(Body::default()),
424 Fragment::Body(Body::from(
425 "Co-authored-by: Billie Thompson <billie@example.com>",
426 )),
427 Fragment::Body(Body::from(
428 "Co-authored-by: Somebody Else <somebody@example.com>",
429 )),
430 ];
431
432 let expected: Trailers<'_> = vec![
433 Trailer::new(
434 "Co-authored-by".into(),
435 "Billie Thompson <billie@example.com>".into(),
436 ),
437 Trailer::new(
438 "Co-authored-by".into(),
439 "Somebody Else <somebody@example.com>".into(),
440 ),
441 ]
442 .into();
443
444 assert_eq!(Trailers::from(trailers), expected);
445 }
446}