Skip to main content

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}