mit-commit 1.14.0

For building commit linters. This makes it eas.
Documentation
use std::convert::TryFrom;
use std::fs::File;
use std::io;
use std::io::Read;
use std::path::PathBuf;

use regex::Regex;
use thiserror::Error;

use crate::scissors::Scissors;
use crate::Trailer;

use super::bodies::Bodies;
use super::body::Body;
use super::comment::Comment;
use super::comments::Comments;
use super::fragment::Fragment;
use super::subject::Subject;
use super::trailers::Trailers;

/// A `CommitMessage`, the primary entry point to the library
#[derive(Debug, PartialEq, Clone)]
pub struct CommitMessage {
    scissors: Option<Scissors>,
    ast: Vec<Fragment>,
    subject: Subject,
    trailers: Trailers,
    comments: Comments,
    bodies: Bodies,
}

impl CommitMessage {
    /// Convert from fragments back into a full `CommitMessage`
    ///
    /// Get back to a `CommitMessage` from an ast, usually after you've been editing the text.
    ///
    /// # Examples
    ///
    /// ```
    /// use indoc::indoc;
    /// use mit_commit::{Bodies, CommitMessage, Subject};
    ///
    /// let message = CommitMessage::from(indoc!(
    ///     "
    ///     Update bashrc to include kubernetes completions
    ///
    ///     This should make it easier to deploy things for the developers.
    ///     Benchmarked with Hyperfine, no noticable performance decrease.
    ///
    ///     ; Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
    ///     ; die mit ';' beginnen, werden ignoriert, und eine leere Beschreibung
    ///     ; bricht den Commit ab.
    ///     ;
    ///     ; Datum:            Sat Jun 27 21:40:14 2020 +0200
    ///     ;
    ///     ; Auf Branch master
    ///     ;
    ///     ; Initialer Commit
    ///     ;
    ///     ; Zum Commit vorgemerkte \u{00E4}nderungen:
    ///     ;    neue Datei:     .bashrc
    ///     ;"
    /// ));
    /// assert_eq!(
    ///     CommitMessage::from_fragments(message.get_ast(), message.get_scissors()),
    ///     message,
    /// )
    /// ```
    #[must_use]
    pub fn from_fragments(fragments: Vec<Fragment>, scissors: Option<Scissors>) -> CommitMessage {
        let body = fragments
            .into_iter()
            .map(|x| match x {
                Fragment::Body(contents) => String::from(contents),
                Fragment::Comment(contents) => String::from(contents),
            })
            .collect::<Vec<String>>()
            .join("\n");

        let scissors: String = if let Some(contents) = scissors {
            format!("\n{}", String::from(contents))
        } else {
            "".into()
        };

        CommitMessage::from(format!("{}{}", body, scissors))
    }

    /// A helper method to let you insert trailers
    ///
    /// # Examples
    ///
    /// ```
    /// use indoc::indoc;
    /// use mit_commit::CommitMessage;
    /// use mit_commit::Trailer;
    /// let commit = CommitMessage::from(indoc!(
    ///     "
    ///     Example Commit Message
    ///
    ///     This is an example commit message for linting
    ///
    ///     Relates-to: #153
    ///
    ///     ; Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
    ///     ; die mit ';' beginnen, werden ignoriert, und eine leere Beschreibung
    ///     ; bricht den Commit ab.
    ///     ;
    ///     ; Auf Branch main
    ///     ; Ihr Branch ist auf demselben Stand wie 'origin/main'.
    ///     ;
    ///     ; Zum Commit vorgemerkte \u{00E4}nderungen:
    ///     ;    neue Datei:     file
    ///     ;
    ///     "
    /// ));
    ///
    /// assert_eq!(
    ///     String::from(commit.add_trailer(Trailer::new(
    ///         "Co-authored-by",
    ///         "Test Trailer <test@example.com>"
    ///     ))),
    ///     String::from(CommitMessage::from(indoc!(
    ///         "
    ///         Example Commit Message
    ///
    ///         This is an example commit message for linting
    ///
    ///         Relates-to: #153
    ///         Co-authored-by: Test Trailer <test@example.com>
    ///
    ///         ; Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
    ///         ; die mit ';' beginnen, werden ignoriert, und eine leere Beschreibung
    ///         ; bricht den Commit ab.
    ///         ;
    ///         ; Auf Branch main
    ///         ; Ihr Branch ist auf demselben Stand wie 'origin/main'.
    ///         ;
    ///         ; Zum Commit vorgemerkte \u{00E4}nderungen:
    ///         ;    neue Datei:     file
    ///         ;
    ///         "
    ///     )))
    /// );
    /// ```
    #[must_use]
    pub fn add_trailer(&self, trailer: Trailer) -> Self {
        let mut fragments = Vec::new();

        if self.bodies.iter().all(Body::is_empty) && self.trailers.is_empty() {
            fragments.push(Fragment::Body(Body::default()));
        }

        if self.trailers.is_empty() {
            fragments.push(Fragment::Body(Body::default()));
        }

        fragments.push(trailer.into());

        self.insert_after_last_full_body(fragments)
    }

    /// Insert text in the place you're most likely to want it
    ///
    /// In case you don't have any full bodies in there, it inserts it at the top of the commit
    /// in the subject line.
    ///
    /// # Examples
    ///
    /// ```
    /// use mit_commit::{Fragment, Body, CommitMessage, Comment};
    ///
    ///         let ast: Vec<Fragment> = vec![
    ///             Fragment::Body(Body::from("Add file")),
    ///             Fragment::Body(Body::default()),
    ///             Fragment::Body(Body::from("Looks-like-a-trailer: But isn\'t")),
    ///             Fragment::Body(Body::default()),
    ///             Fragment::Body(Body::from("This adds file primarily for demonstration purposes. It might not be\nuseful as an actual commit, but it\'s very useful as a example to use in\ntests.")),
    ///             Fragment::Body(Body::default()),
    ///             Fragment::Body(Body::from("Relates-to: #128")),
    ///             Fragment::Body(Body::default()),
    ///             Fragment::Comment(Comment::from("# Short (50 chars or less) summary of changes\n#\n# More detailed explanatory text, if necessary.  Wrap it to\n# about 72 characters or so.  In some contexts, the first\n# line is treated as the subject of an email and the rest of\n# the text as the body.  The blank line separating the\n# summary from the body is critical (unless you omit the body\n# entirely); tools like rebase can get confused if you run\n# the two together.\n#\n# Further paragraphs come after blank lines.\n#\n#   - Bullet points are okay, too\n#\n#   - Typically a hyphen or asterisk is used for the bullet,\n#     preceded by a single space, with blank lines in\n#     between, but conventions vary here")),
    ///             Fragment::Body(Body::default()),
    ///             Fragment::Comment(Comment::from("# Bitte geben Sie eine Commit-Beschreibung f\u{fc}r Ihre \u{e4}nderungen ein. Zeilen,\n# die mit \'#\' beginnen, werden ignoriert, und eine leere Beschreibung\n# bricht den Commit ab.\n#\n# Auf Branch main\n# Ihr Branch ist auf demselben Stand wie \'origin/main\'.\n#\n# Zum Commit vorgemerkte \u{e4}nderungen:\n#\tneue Datei:     file\n#"))
    ///         ];
    ///         let commit = CommitMessage::from_fragments(ast, None);
    ///
    ///         assert_eq!(commit.insert_after_last_full_body(vec![Fragment::Body(Body::from("Relates-to: #656"))]).get_ast(), vec![
    ///             Fragment::Body(Body::from("Add file")),
    ///             Fragment::Body(Body::default()),
    ///             Fragment::Body(Body::from("Looks-like-a-trailer: But isn\'t")),
    ///             Fragment::Body(Body::default()),
    ///             Fragment::Body(Body::from("This adds file primarily for demonstration purposes. It might not be\nuseful as an actual commit, but it\'s very useful as a example to use in\ntests.")),
    ///             Fragment::Body(Body::default()),
    ///             Fragment::Body(Body::from("Relates-to: #128\nRelates-to: #656")),
    ///             Fragment::Body(Body::default()),
    ///             Fragment::Comment(Comment::from("# Short (50 chars or less) summary of changes\n#\n# More detailed explanatory text, if necessary.  Wrap it to\n# about 72 characters or so.  In some contexts, the first\n# line is treated as the subject of an email and the rest of\n# the text as the body.  The blank line separating the\n# summary from the body is critical (unless you omit the body\n# entirely); tools like rebase can get confused if you run\n# the two together.\n#\n# Further paragraphs come after blank lines.\n#\n#   - Bullet points are okay, too\n#\n#   - Typically a hyphen or asterisk is used for the bullet,\n#     preceded by a single space, with blank lines in\n#     between, but conventions vary here")),
    ///             Fragment::Body(Body::default()),
    ///             Fragment::Comment(Comment::from("# Bitte geben Sie eine Commit-Beschreibung f\u{fc}r Ihre \u{e4}nderungen ein. Zeilen,\n# die mit \'#\' beginnen, werden ignoriert, und eine leere Beschreibung\n# bricht den Commit ab.\n#\n# Auf Branch main\n# Ihr Branch ist auf demselben Stand wie \'origin/main\'.\n#\n# Zum Commit vorgemerkte \u{e4}nderungen:\n#\tneue Datei:     file\n#"))
    ///         ])
    /// ```
    #[must_use]
    pub fn insert_after_last_full_body(&self, fragment: Vec<Fragment>) -> CommitMessage {
        let position = self.ast.iter().rposition(|fragment| match fragment {
            Fragment::Body(body) => !body.is_empty(),
            Fragment::Comment(_) => false,
        });

        let (before, after): (Vec<_>, Vec<_>) = match position {
            Some(position) => self
                .ast
                .clone()
                .into_iter()
                .enumerate()
                .partition(|(index, _)| index <= &position),
            None => (vec![], self.ast.clone().into_iter().enumerate().collect()),
        };

        CommitMessage::from_fragments(
            [
                before.into_iter().map(|(_, x)| x).collect(),
                fragment,
                after.into_iter().map(|(_, x)| x).collect(),
            ]
            .concat(),
            self.get_scissors(),
        )
    }

    fn convert_to_per_line_ast(comment_character: Option<char>, rest: &str) -> Vec<Fragment> {
        rest.lines()
            .map(|line| match comment_character {
                Some(comment_character) => {
                    if line.starts_with(comment_character) {
                        Fragment::Comment(Comment::from(line))
                    } else {
                        Fragment::Body(Body::from(line))
                    }
                }
                None => Fragment::Body(Body::from(line)),
            })
            .collect()
    }

    fn group_ast(ungrouped_ast: Vec<Fragment>) -> Vec<Fragment> {
        ungrouped_ast
            .into_iter()
            .fold(vec![], |acc: Vec<Fragment>, new_fragment| {
                let mut previous_fragments = acc.clone();
                match (acc.last(), &new_fragment) {
                    (None, fragment) => {
                        previous_fragments.push(fragment.clone());
                        previous_fragments
                    }
                    (Some(Fragment::Comment(existing)), Fragment::Comment(new)) => {
                        previous_fragments.truncate(acc.len() - 1);
                        previous_fragments.push(Fragment::Comment(existing.append(&new)));
                        previous_fragments
                    }
                    (Some(Fragment::Body(existing)), Fragment::Body(new)) => {
                        if new.is_empty() || existing.is_empty() {
                            previous_fragments.push(Fragment::Body(new.clone()));
                            previous_fragments
                        } else {
                            previous_fragments.truncate(acc.len() - 1);
                            previous_fragments.push(Fragment::Body(existing.append(&new)));
                            previous_fragments
                        }
                    }
                    (Some(Fragment::Body(_)), Fragment::Comment(new)) => {
                        previous_fragments.push(Fragment::Comment(new.clone()));
                        previous_fragments
                    }
                    (Some(Fragment::Comment(_)), Fragment::Body(new)) => {
                        previous_fragments.push(Fragment::Body(new.clone()));
                        previous_fragments
                    }
                }
            })
    }

    /// Get the `Subject` line from the `CommitMessage`
    ///
    /// It's possible to get this from the ast, but it's a bit of a faff, so this is a convencience
    /// method
    ///
    /// # Examples
    ///
    /// ```
    /// use indoc::indoc;
    /// use mit_commit::{Bodies, CommitMessage, Subject};
    ///
    /// let message = CommitMessage::from(indoc!(
    ///     "
    ///     Update bashrc to include kubernetes completions
    ///
    ///     This should make it easier to deploy things for the developers.
    ///     Benchmarked with Hyperfine, no noticable performance decrease.
    ///
    ///     ; Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
    ///     ; die mit ';' beginnen, werden ignoriert, und eine leere Beschreibung
    ///     ; bricht den Commit ab.
    ///     ;
    ///     ; Datum:            Sat Jun 27 21:40:14 2020 +0200
    ///     ;
    ///     ; Auf Branch master
    ///     ;
    ///     ; Initialer Commit
    ///     ;
    ///     ; Zum Commit vorgemerkte \u{00E4}nderungen:
    ///     ;    neue Datei:     .bashrc
    ///     ;"
    /// ));
    /// assert_eq!(
    ///     message.get_subject(),
    ///     Subject::from("Update bashrc to include kubernetes completions")
    /// )
    /// ```
    #[must_use]
    pub fn get_subject(&self) -> Subject {
        self.subject.clone()
    }

    /// Get the underlying data structure that represents the `CommitMessage`
    ///
    /// This is the underlying datastructure for the commit. You might want this to create a
    /// complicated linter, or modify the `CommitMessage` to your liking.
    ///
    /// Notice how it doesn't include the `Scissors` section.
    ///
    /// # Examples
    ///
    /// ```
    /// use indoc::indoc;
    /// use mit_commit::{Body, CommitMessage, Fragment, Trailer, Trailers, Comment};
    ///
    /// let message = CommitMessage::from(indoc!(
    ///     "
    ///     Add file
    ///
    ///     Looks-like-a-trailer: But isn't
    ///
    ///     This adds file primarily for demonstration purposes. It might not be
    ///     useful as an actual commit, but it's very useful as a example to use in
    ///     tests.
    ///
    ///     Relates-to: #128
    ///     Relates-to: #129
    ///
    ///     ; Short (50 chars or less) summary of changes
    ///     ;
    ///     ; More detailed explanatory text, if necessary.  Wrap it to
    ///     ; about 72 characters or so.  In some contexts, the first
    ///     ; line is treated as the subject of an email and the rest of
    ///     ; the text as the body.  The blank line separating the
    ///     ; summary from the body is critical (unless you omit the body
    ///     ; entirely); tools like rebase can get confused if you run
    ///     ; the two together.
    ///     ;
    ///     ; Further paragraphs come after blank lines.
    ///     ;
    ///     ;   - Bullet points are okay, too
    ///     ;
    ///     ;   - Typically a hyphen or asterisk is used for the bullet,
    ///     ;     preceded by a single space, with blank lines in
    ///     ;     between, but conventions vary here
    ///
    ///     ; Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
    ///     ; die mit ';' beginnen, werden ignoriert, und eine leere Beschreibung
    ///     ; bricht den Commit ab.
    ///     ;
    ///     ; Auf Branch main
    ///     ; Ihr Branch ist auf demselben Stand wie 'origin/main'.
    ///     ;
    ///     ; Zum Commit vorgemerkte \u{00E4}nderungen:
    ///     ;   neue Datei:     file
    ///     ;
    ///     ; ------------------------ >8 ------------------------
    ///     ; \u{00E4}ndern oder entfernen Sie nicht die obige Zeile.
    ///     ; Alles unterhalb von ihr wird ignoriert.
    ///     diff --git a/file b/file
    ///     new file mode 100644
    ///     index 0000000..e69de29
    ///     "
    /// ));
    /// let ast = vec![
    ///     Fragment::Body(Body::from("Add file")),
    ///     Fragment::Body(Body::default()),
    ///     Fragment::Body(Body::from("Looks-like-a-trailer: But isn't")),
    ///     Fragment::Body(Body::default()),
    ///     Fragment::Body(Body::from("This adds file primarily for demonstration purposes. It might not be\nuseful as an actual commit, but it\'s very useful as a example to use in\ntests.")),
    ///     Fragment::Body(Body::default()),
    ///     Fragment::Body(Body::from("Relates-to: #128\nRelates-to: #129")),
    ///     Fragment::Body(Body::default()),
    ///     Fragment::Comment(Comment::from("; Short (50 chars or less) summary of changes\n;\n; More detailed explanatory text, if necessary.  Wrap it to\n; about 72 characters or so.  In some contexts, the first\n; line is treated as the subject of an email and the rest of\n; the text as the body.  The blank line separating the\n; summary from the body is critical (unless you omit the body\n; entirely); tools like rebase can get confused if you run\n; the two together.\n;\n; Further paragraphs come after blank lines.\n;\n;   - Bullet points are okay, too\n;\n;   - Typically a hyphen or asterisk is used for the bullet,\n;     preceded by a single space, with blank lines in\n;     between, but conventions vary here")),
    ///     Fragment::Body(Body::default()),
    ///     Fragment::Comment(Comment::from("; Bitte geben Sie eine Commit-Beschreibung für Ihre änderungen ein. Zeilen,\n; die mit \';\' beginnen, werden ignoriert, und eine leere Beschreibung\n; bricht den Commit ab.\n;\n; Auf Branch main\n; Ihr Branch ist auf demselben Stand wie \'origin/main\'.\n;\n; Zum Commit vorgemerkte änderungen:\n;   neue Datei:     file\n;"))
    /// ];
    /// assert_eq!(message.get_ast(), ast)
    /// ```
    #[must_use]
    pub fn get_ast(&self) -> Vec<Fragment> {
        self.ast.clone()
    }

    /// Get the `Bodies` from the `CommitMessage`
    ///
    /// This gets the bodies from the commit message in easy to use paragraphs, we add in blank
    /// bodies because starting a new paragraph is a visual delimiter so we want to make that easy
    /// to detect.
    ///
    /// It doesn't include the `Subject` line, but if there's a blank line after it (as is
    /// recommended by the manual), the bodies will start with a new empty body.
    ///
    /// # Examples
    ///
    /// ```
    /// use indoc::indoc;
    /// use mit_commit::{Bodies, Body, CommitMessage, Subject};
    ///
    /// let message = CommitMessage::from(indoc!(
    ///     "
    ///     Update bashrc to include kubernetes completions
    ///
    ///     This should make it easier to deploy things for the developers.
    ///     Benchmarked with Hyperfine, no noticable performance decrease.
    ///
    ///     I am unsure as to why this wasn't being automatically discovered from Brew.
    ///     I've filed a bug report with them.
    ///
    ///     ; Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
    ///     ; die mit ';' beginnen, werden ignoriert, und eine leere Beschreibung
    ///     ; bricht den Commit ab.
    ///     ;
    ///     ; Datum:            Sat Jun 27 21:40:14 2020 +0200
    ///     ;
    ///     ; Auf Branch master
    ///     ;
    ///     ; Initialer Commit
    ///     ;
    ///     ; Zum Commit vorgemerkte \u{00E4}nderungen:
    ///     ;    neue Datei:     .bashrc
    ///     ;"
    /// ));
    /// let bodies = vec![
    ///     Body::default(),
    ///     Body::from(indoc!(
    ///         "
    ///         This should make it easier to deploy things for the developers.
    ///         Benchmarked with Hyperfine, no noticable performance decrease."
    ///     )),
    ///     Body::default(),
    ///     Body::from(indoc!(
    ///         "
    ///         I am unsure as to why this wasn't being automatically discovered from Brew.
    ///         I've filed a bug report with them."
    ///     )),
    /// ];
    /// assert_eq!(message.get_body(), Bodies::from(bodies))
    /// ```
    #[must_use]
    pub fn get_body(&self) -> Bodies {
        self.bodies.clone()
    }

    /// Get the `Comments` from the `CommitMessage`
    ///
    /// We this will get you all the comments before the `Scissors` section. The `Scissors` section
    /// is the bit that appears when you run `git commit --verbose`, that contains the diffs.
    ///
    /// If there's `Comment` mixed in with the body, it'll return those too, but not any of the
    /// `Body` aound them.
    ///
    /// # Examples
    ///
    /// ```
    /// use indoc::indoc;
    /// use mit_commit::{Body, Comment, Comments, CommitMessage, Subject};
    ///
    /// let message = CommitMessage::from(indoc!(
    ///     "
    ///     Update bashrc to include kubernetes completions
    ///
    ///     This should make it easier to deploy things for the developers.
    ///     Benchmarked with Hyperfine, no noticable performance decrease.
    ///
    ///     I am unsure as to why this wasn't being automatically discovered from Brew.
    ///     I've filed a bug report with them.
    ///
    ///     ; Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
    ///     ; die mit ';' beginnen, werden ignoriert, und eine leere Beschreibung
    ///     ; bricht den Commit ab.
    ///     ;
    ///     ; Datum:            Sat Jun 27 21:40:14 2020 +0200
    ///     ;
    ///     ; Auf Branch master
    ///     ;
    ///     ; Initialer Commit
    ///     ;
    ///     ; Zum Commit vorgemerkte \u{00E4}nderungen:
    ///     ;    neue Datei:     .bashrc
    ///     ;"
    /// ));
    /// let comments = vec![Comment::from(indoc!(
    ///     "
    ///     ; Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
    ///     ; die mit ';' beginnen, werden ignoriert, und eine leere Beschreibung
    ///     ; bricht den Commit ab.
    ///     ;
    ///     ; Datum:            Sat Jun 27 21:40:14 2020 +0200
    ///     ;
    ///     ; Auf Branch master
    ///     ;
    ///     ; Initialer Commit
    ///     ;
    ///     ; Zum Commit vorgemerkte \u{00E4}nderungen:
    ///     ;    neue Datei:     .bashrc
    ///     ;"
    /// ))];
    /// assert_eq!(message.get_comments(), Comments::from(comments))
    /// ```
    #[must_use]
    pub fn get_comments(&self) -> Comments {
        self.comments.clone()
    }

    /// Get the `Scissors` from the `CommitMessage`
    ///
    /// We this will get you all the comments in the `Scissors` section. The `Scissors` section
    /// is the bit that appears when you run `git commit --verbose`, that contains the diffs, and
    /// is not preserved when you save the commit.
    ///
    ///
    /// # Examples
    ///
    /// ```
    /// use indoc::indoc;
    /// use mit_commit::{Body, CommitMessage, Scissors, Subject};
    ///
    /// let message = CommitMessage::from(indoc!(
    ///     "
    ///     Add file
    ///
    ///     This adds file primarily for demonstration purposes. It might not be
    ///     useful as an actual commit, but it's very useful as a example to use in
    ///     tests.
    ///
    ///     Relates-to: #128
    ///
    ///     ; Short (50 chars or less) summary of changes
    ///     ;
    ///     ; More detailed explanatory text, if necessary.  Wrap it to
    ///     ; about 72 characters or so.  In some contexts, the first
    ///     ; line is treated as the subject of an email and the rest of
    ///     ; the text as the body.  The blank line separating the
    ///     ; summary from the body is critical (unless you omit the body
    ///     ; entirely); tools like rebase can get confused if you run
    ///     ; the two together.
    ///     ;
    ///     ; Further paragraphs come after blank lines.
    ///     ;
    ///     ;   - Bullet points are okay, too
    ///     ;
    ///     ;   - Typically a hyphen or asterisk is used for the bullet,
    ///     ;     preceded by a single space, with blank lines in
    ///     ;     between, but conventions vary here
    ///
    ///     ; Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
    ///     ; die mit ';' beginnen, werden ignoriert, und eine leere Beschreibung
    ///     ; bricht den Commit ab.
    ///     ;
    ///     ; Auf Branch main
    ///     ; Ihr Branch ist auf demselben Stand wie 'origin/main'.
    ///     ;
    ///     ; Zum Commit vorgemerkte \u{00E4}nderungen:
    ///     ;   neue Datei:     file
    ///     ;
    ///     ; ------------------------ >8 ------------------------
    ///     ; \u{00E4}ndern oder entfernen Sie nicht die obige Zeile.
    ///     ; Alles unterhalb von ihr wird ignoriert.
    ///     diff --git a/file b/file
    ///     new file mode 100644
    ///     index 0000000..e69de29
    ///     "
    /// ));
    /// let scissors = Scissors::from(indoc!(
    ///     "
    ///     ; ------------------------ >8 ------------------------
    ///     ; \u{00E4}ndern oder entfernen Sie nicht die obige Zeile.
    ///     ; Alles unterhalb von ihr wird ignoriert.
    ///     diff --git a/file b/file
    ///     new file mode 100644
    ///     index 0000000..e69de29
    ///     "
    /// ));
    /// assert_eq!(message.get_scissors(), Some(scissors))
    /// ```
    #[must_use]
    pub fn get_scissors(&self) -> Option<Scissors> {
        self.scissors.clone()
    }

    /// Get the `Scissors` from the `CommitMessage`
    ///
    /// We this will get you all the comments in the `Scissors` section. The `Scissors` section
    /// is the bit that appears when you run `git commit --verbose`, that contains the diffs, and
    /// is not preserved when you save the commit.
    ///
    ///
    /// # Examples
    ///
    /// ```
    /// use indoc::indoc;
    /// use mit_commit::{Body, CommitMessage, Trailer, Trailers};
    ///
    /// let message = CommitMessage::from(indoc!(
    ///     "
    ///     Add file
    ///
    ///     Looks-like-a-trailer: But isn't
    ///
    ///     This adds file primarily for demonstration purposes. It might not be
    ///     useful as an actual commit, but it's very useful as a example to use in
    ///     tests.
    ///
    ///     Relates-to: #128
    ///     Relates-to: #129
    ///
    ///     ; Short (50 chars or less) summary of changes
    ///     ;
    ///     ; More detailed explanatory text, if necessary.  Wrap it to
    ///     ; about 72 characters or so.  In some contexts, the first
    ///     ; line is treated as the subject of an email and the rest of
    ///     ; the text as the body.  The blank line separating the
    ///     ; summary from the body is critical (unless you omit the body
    ///     ; entirely); tools like rebase can get confused if you run
    ///     ; the two together.
    ///     ;
    ///     ; Further paragraphs come after blank lines.
    ///     ;
    ///     ;   - Bullet points are okay, too
    ///     ;
    ///     ;   - Typically a hyphen or asterisk is used for the bullet,
    ///     ;     preceded by a single space, with blank lines in
    ///     ;     between, but conventions vary here
    ///
    ///     ; Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
    ///     ; die mit ';' beginnen, werden ignoriert, und eine leere Beschreibung
    ///     ; bricht den Commit ab.
    ///     ;
    ///     ; Auf Branch main
    ///     ; Ihr Branch ist auf demselben Stand wie 'origin/main'.
    ///     ;
    ///     ; Zum Commit vorgemerkte \u{00E4}nderungen:
    ///     ;   neue Datei:     file
    ///     ;
    ///     ; ------------------------ >8 ------------------------
    ///     ; \u{00E4}ndern oder entfernen Sie nicht die obige Zeile.
    ///     ; Alles unterhalb von ihr wird ignoriert.
    ///     diff --git a/file b/file
    ///     new file mode 100644
    ///     index 0000000..e69de29
    ///     "
    /// ));
    /// let trailers = vec![
    ///     Trailer::new("Relates-to", "#128"),
    ///     Trailer::new("Relates-to", "#129"),
    /// ];
    /// assert_eq!(message.get_trailers(), Trailers::from(trailers))
    /// ```
    #[must_use]
    pub fn get_trailers(&self) -> Trailers {
        self.trailers.clone()
    }

    /// Does the commit match the saved portions of the commit
    ///
    /// This takes a regex and matches it to the visible portions of the commits, so it excludes
    /// comments, and everything after the scissors.
    ///
    /// # Examples
    ///
    /// ```
    /// use indoc::indoc;
    /// use mit_commit::CommitMessage;
    /// use regex::Regex;
    ///
    /// let commit = CommitMessage::from(indoc!(
    ///     "
    ///     Example Commit Message
    ///
    ///     This is an example commit message for linting
    ///
    ///
    ///     ; Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
    ///     ; die mit ';' beginnen, werden ignoriert, und eine leere Beschreibung
    ///     ; bricht den Commit ab.
    ///     ;
    ///     ; Auf Branch main
    ///     ; Ihr Branch ist auf demselben Stand wie 'origin/main'.
    ///     ;
    ///     ; Zum Commit vorgemerkte \u{00E4}nderungen:
    ///     ;    neue Datei:     file
    ///     ;
    ///     "
    /// ));
    ///
    /// let re = Regex::new("[Bb]itte").unwrap();
    /// assert_eq!(commit.matches_pattern(&re), false);
    ///
    /// let re = Regex::new("f[o\u{00FC}]r linting").unwrap();
    /// assert_eq!(commit.matches_pattern(&re), true);
    ///
    /// let re = Regex::new("[Ee]xample Commit Message").unwrap();
    /// assert_eq!(commit.matches_pattern(&re), true);
    /// ```
    pub fn matches_pattern(&self, re: &Regex) -> bool {
        let text = self
            .clone()
            .get_ast()
            .into_iter()
            .filter_map(|fragment| match fragment {
                Fragment::Body(body) => Some(String::from(body)),
                Fragment::Comment(_) => None,
            })
            .collect::<Vec<_>>()
            .join("\n");
        re.is_match(&text)
    }
}

impl From<CommitMessage> for String {
    fn from(commit_message: CommitMessage) -> Self {
        let basic_commit = commit_message
            .get_ast()
            .iter()
            .map(|item| match item {
                Fragment::Body(contents) => String::from(contents.clone()),
                Fragment::Comment(contents) => String::from(contents.clone()),
            })
            .collect::<Vec<_>>()
            .join("\n");

        if let Some(scissors) = commit_message.get_scissors() {
            format!("{}\n{}", basic_commit, String::from(scissors))
        } else {
            basic_commit
        }
    }
}

impl From<&str> for CommitMessage {
    /// Create a new `CommitMessage`
    ///
    /// Create a commit message from a string. It's expected that you'll be reading this during some
    /// sort of Git Hook
    ///
    /// # Examples
    ///
    /// ```
    /// use indoc::indoc;
    /// use mit_commit::{Bodies, CommitMessage, Subject};
    ///
    /// let message = CommitMessage::from(indoc!(
    ///     "
    ///     Update bashrc to include kubernetes completions
    ///
    ///     This should make it easier to deploy things for the developers.
    ///     Benchmarked with Hyperfine, no noticable performance decrease.
    ///
    ///     ; Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
    ///     ; die mit ';' beginnen, werden ignoriert, und eine leere Beschreibung
    ///     ; bricht den Commit ab.
    ///     ;
    ///     ; Datum:            Sat Jun 27 21:40:14 2020 +0200
    ///     ;
    ///     ; Auf Branch master
    ///     ;
    ///     ; Initialer Commit
    ///     ;
    ///     ; Zum Commit vorgemerkte \u{00E4}nderungen:
    ///     ;    neue Datei:     .bashrc
    ///     ;"
    /// ));
    /// assert_eq!(
    ///     message.get_subject(),
    ///     Subject::from("Update bashrc to include kubernetes completions")
    /// )
    fn from(message: &str) -> Self {
        let (rest, scissors) = Scissors::parse_sections(message);
        let comment_character = CommitMessage::guess_comment_character(&rest, scissors.clone());
        let per_line_ast = CommitMessage::convert_to_per_line_ast(comment_character, &rest);
        let trailers = per_line_ast.clone().into();
        let mut ast: Vec<Fragment> = CommitMessage::group_ast(per_line_ast);

        if let (None, Some('\n')) = (scissors.clone(), message.chars().last()) {
            ast.push(Fragment::Body(Body::default()))
        }

        let subject = Subject::from(ast.clone());
        let comments = Comments::from(ast.clone());
        let bodies = Bodies::from(ast.clone());

        CommitMessage {
            ast,
            subject,
            bodies,
            comments,
            trailers,
            scissors,
        }
    }
}

impl TryFrom<PathBuf> for CommitMessage {
    type Error = Error;

    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
        let mut file = File::open(value)?;
        let mut buffer = String::new();

        file.read_to_string(&mut buffer)
            .map_err(Error::from)
            .map(move |_| CommitMessage::from(buffer))
    }
}

impl From<String> for CommitMessage {
    fn from(message: String) -> Self {
        let str: &str = &message;
        CommitMessage::from(str)
    }
}

#[derive(Error, Debug)]
pub enum Error {
    #[error("failed to read commit file {0}")]
    Io(#[from] io::Error),
}

#[cfg(test)]
mod tests {
    use indoc::indoc;
    use pretty_assertions::assert_eq;
    use regex::Regex;

    use crate::bodies::Bodies;
    use crate::body::Body;
    use crate::comment::Comment;
    use crate::comments::Comments;
    use crate::scissors::Scissors;
    use crate::subject::Subject;
    use crate::trailer::Trailer;
    use crate::trailers::Trailers;
    use crate::Fragment;

    use super::CommitMessage;

    #[test]
    fn can_check_if_it_matches_pattern() {
        let commit = CommitMessage::from(indoc!(
            "
            Example Commit Message

            This is an example commit message for linting

            Relates-to: #153
            # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
            # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
            # bricht den Commit ab.
            #
            # Auf Branch main
            # Ihr Branch ist auf demselben Stand wie 'origin/main'.
            #
            # Zum Commit vorgemerkte \u{00E4}nderungen:
            #	neue Datei:     file
            #
            "
        ));

        let re = Regex::new("[Bb]itte").unwrap();
        assert_eq!(commit.matches_pattern(&re), false);

        let re = Regex::new("f[o\u{00FC}]r linting").unwrap();
        assert_eq!(commit.matches_pattern(&re), true);

        let re = Regex::new("[Ee]xample Commit Message").unwrap();
        assert_eq!(commit.matches_pattern(&re), true);

        let re = Regex::new("Relates[- ]to").unwrap();
        assert_eq!(commit.matches_pattern(&re), true);
    }

    #[test]
    fn can_add_trailers_to_a_normal_commit() {
        let commit = CommitMessage::from(indoc!(
            "
            Example Commit Message

            This is an example commit message for linting

            Relates-to: #153

            # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
            # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
            # bricht den Commit ab.
            #
            # Auf Branch main
            # Ihr Branch ist auf demselben Stand wie 'origin/main'.
            #
            # Zum Commit vorgemerkte \u{00E4}nderungen:
            #	neue Datei:     file
            #
            "
        ));

        assert_eq!(
            String::from(commit.add_trailer(Trailer::new("Co-authored-by", "Test Trailer <test@example.com>"))),
            String::from(CommitMessage::from(indoc!(
            "
            Example Commit Message

            This is an example commit message for linting

            Relates-to: #153
            Co-authored-by: Test Trailer <test@example.com>

            # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
            # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
            # bricht den Commit ab.
            #
            # Auf Branch main
            # Ihr Branch ist auf demselben Stand wie 'origin/main'.
            #
            # Zum Commit vorgemerkte \u{00E4}nderungen:
            #	neue Datei:     file
            #
            "
        ))));
    }

    #[test]
    fn can_add_trailers_to_a_commit_without_existing_trailers() {
        let commit = CommitMessage::from(indoc!(
            "
            Example Commit Message

            This is an example commit message for linting

            # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
            # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
            # bricht den Commit ab.
            #
            # Auf Branch main
            # Ihr Branch ist auf demselben Stand wie 'origin/main'.
            #
            # Zum Commit vorgemerkte \u{00E4}nderungen:
            #	neue Datei:     file
            #
            "
        ));

        let expected = CommitMessage::from(indoc!(
            "
            Example Commit Message

            This is an example commit message for linting

            Co-authored-by: Test Trailer <test@example.com>

            # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
            # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
            # bricht den Commit ab.
            #
            # Auf Branch main
            # Ihr Branch ist auf demselben Stand wie 'origin/main'.
            #
            # Zum Commit vorgemerkte \u{00E4}nderungen:
            #	neue Datei:     file
            #
            "
        ));
        assert_eq!(
            String::from(commit.add_trailer(Trailer::new(
                "Co-authored-by",
                "Test Trailer <test@example.com>"
            ))),
            String::from(expected)
        );
    }

    #[test]
    fn can_add_trailers_to_an_empty_commit() {
        let commit = CommitMessage::from(indoc!(
            "

            # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
            # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
            # bricht den Commit ab.
            #
            # Auf Branch main
            # Ihr Branch ist auf demselben Stand wie 'origin/main'.
            #
            # Zum Commit vorgemerkte \u{00E4}nderungen:
            #	neue Datei:     file
            #
            "
        ));

        let expected = CommitMessage::from(indoc!(
            "


            Co-authored-by: Test Trailer <test@example.com>

            # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
            # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
            # bricht den Commit ab.
            #
            # Auf Branch main
            # Ihr Branch ist auf demselben Stand wie 'origin/main'.
            #
            # Zum Commit vorgemerkte \u{00E4}nderungen:
            #	neue Datei:     file
            #
            "
        ));
        assert_eq!(
            String::from(commit.add_trailer(Trailer::new(
                "Co-authored-by",
                "Test Trailer <test@example.com>"
            ))),
            String::from(expected)
        );
    }

    #[test]
    fn can_add_trailers_to_an_empty_commit_with_single_trailer() {
        let commit = CommitMessage::from(indoc!(
            "


            Co-authored-by: Test Trailer <test@example.com>

            # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
            # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
            # bricht den Commit ab.
            #
            # Auf Branch main
            # Ihr Branch ist auf demselben Stand wie 'origin/main'.
            #
            # Zum Commit vorgemerkte \u{00E4}nderungen:
            #	neue Datei:     file
            #
            "
        ));

        let expected = CommitMessage::from(indoc!(
            "


            Co-authored-by: Test Trailer <test@example.com>
            Co-authored-by: Someone Else <someone@example.com>

            # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
            # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
            # bricht den Commit ab.
            #
            # Auf Branch main
            # Ihr Branch ist auf demselben Stand wie 'origin/main'.
            #
            # Zum Commit vorgemerkte \u{00E4}nderungen:
            #	neue Datei:     file
            #
            "
        ));
        assert_eq!(
            String::from(commit.add_trailer(Trailer::new(
                "Co-authored-by",
                "Someone Else <someone@example.com>"
            ))),
            String::from(expected)
        );
    }

    #[test]
    fn can_generate_a_commit_from_an_ast() {
        let message = CommitMessage::from_fragments(
            vec![
                Fragment::Body(Body::from("Example Commit")),
                Fragment::Body(Body::default()),
                Fragment::Body(Body::from("Here is a body")),
                Fragment::Comment(Comment::from("# Example Commit")),
            ],
            Some(Scissors::from(indoc!(
                "
                # ------------------------ >8 ------------------------
                # \u{00E4}ndern oder entfernen Sie nicht die obige Zeile.
                # Alles unterhalb von ihr wird ignoriert.
                diff --git a/file b/file
                new file mode 100644
                index 0000000..e69de29
                "
            ))),
        );

        assert_eq!(
            String::from(message),
            String::from(indoc!(
                "
                Example Commit

                Here is a body
                # Example Commit
                # ------------------------ >8 ------------------------
                # \u{00E4}ndern oder entfernen Sie nicht die obige Zeile.
                # Alles unterhalb von ihr wird ignoriert.
                diff --git a/file b/file
                new file mode 100644
                index 0000000..e69de29
                "
            ))
        )
    }

    const COMMIT_WITH_ALL_FEATURES: &str = indoc!(
        "
        Add file

        Looks-like-a-trailer: But isn't

        This adds file primarily for demonstration purposes. It might not be
        useful as an actual commit, but it's very useful as a example to use in
        tests.

        Relates-to: #128

        # Short (50 chars or less) summary of changes
        #
        # More detailed explanatory text, if necessary.  Wrap it to
        # about 72 characters or so.  In some contexts, the first
        # line is treated as the subject of an email and the rest of
        # the text as the body.  The blank line separating the
        # summary from the body is critical (unless you omit the body
        # entirely); tools like rebase can get confused if you run
        # the two together.
        #
        # Further paragraphs come after blank lines.
        #
        #   - Bullet points are okay, too
        #
        #   - Typically a hyphen or asterisk is used for the bullet,
        #     preceded by a single space, with blank lines in
        #     between, but conventions vary here

        # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
        # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
        # bricht den Commit ab.
        #
        # Auf Branch main
        # Ihr Branch ist auf demselben Stand wie 'origin/main'.
        #
        # Zum Commit vorgemerkte \u{00E4}nderungen:
        #	neue Datei:     file
        #
        # ------------------------ >8 ------------------------
        # \u{00E4}ndern oder entfernen Sie nicht die obige Zeile.
        # Alles unterhalb von ihr wird ignoriert.
        diff --git a/file b/file
        new file mode 100644
        index 0000000..e69de29
        "
    );

    #[test]
    fn can_reliably_parse_from_commit_with_all_features() {
        let first_commit_message = CommitMessage::from(COMMIT_WITH_ALL_FEATURES);
        let string_version_of_commit = String::from(first_commit_message.clone());
        let second_commit_message = CommitMessage::from(string_version_of_commit.clone());

        assert_eq!(string_version_of_commit, COMMIT_WITH_ALL_FEATURES);
        assert_eq!(first_commit_message, second_commit_message)
    }

    #[test]
    fn can_get_ast_from_commit_with_all_features() {
        let message = CommitMessage::from(COMMIT_WITH_ALL_FEATURES);
        let ast: Vec<Fragment> = vec![
            Fragment::Body(Body::from("Add file")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("Looks-like-a-trailer: But isn\'t")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("This adds file primarily for demonstration purposes. It might not be\nuseful as an actual commit, but it\'s very useful as a example to use in\ntests.")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("Relates-to: #128")),
            Fragment::Body(Body::default()),
            Fragment::Comment(Comment::from("# Short (50 chars or less) summary of changes\n#\n# More detailed explanatory text, if necessary.  Wrap it to\n# about 72 characters or so.  In some contexts, the first\n# line is treated as the subject of an email and the rest of\n# the text as the body.  The blank line separating the\n# summary from the body is critical (unless you omit the body\n# entirely); tools like rebase can get confused if you run\n# the two together.\n#\n# Further paragraphs come after blank lines.\n#\n#   - Bullet points are okay, too\n#\n#   - Typically a hyphen or asterisk is used for the bullet,\n#     preceded by a single space, with blank lines in\n#     between, but conventions vary here")),
            Fragment::Body(Body::default()),
            Fragment::Comment(Comment::from("# Bitte geben Sie eine Commit-Beschreibung f\u{fc}r Ihre \u{e4}nderungen ein. Zeilen,\n# die mit \'#\' beginnen, werden ignoriert, und eine leere Beschreibung\n# bricht den Commit ab.\n#\n# Auf Branch main\n# Ihr Branch ist auf demselben Stand wie \'origin/main\'.\n#\n# Zum Commit vorgemerkte \u{e4}nderungen:\n#\tneue Datei:     file\n#"))
        ];

        assert_eq!(message.get_ast(), ast)
    }

    #[test]
    fn insert_after_last_body() {
        let ast: Vec<Fragment> = vec![
            Fragment::Body(Body::from("Add file")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("Looks-like-a-trailer: But isn\'t")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("This adds file primarily for demonstration purposes. It might not be\nuseful as an actual commit, but it\'s very useful as a example to use in\ntests.")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("Relates-to: #128")),
            Fragment::Body(Body::default()),
            Fragment::Comment(Comment::from("# Short (50 chars or less) summary of changes\n#\n# More detailed explanatory text, if necessary.  Wrap it to\n# about 72 characters or so.  In some contexts, the first\n# line is treated as the subject of an email and the rest of\n# the text as the body.  The blank line separating the\n# summary from the body is critical (unless you omit the body\n# entirely); tools like rebase can get confused if you run\n# the two together.\n#\n# Further paragraphs come after blank lines.\n#\n#   - Bullet points are okay, too\n#\n#   - Typically a hyphen or asterisk is used for the bullet,\n#     preceded by a single space, with blank lines in\n#     between, but conventions vary here")),
            Fragment::Body(Body::default()),
            Fragment::Comment(Comment::from("# Bitte geben Sie eine Commit-Beschreibung f\u{fc}r Ihre \u{e4}nderungen ein. Zeilen,\n# die mit \'#\' beginnen, werden ignoriert, und eine leere Beschreibung\n# bricht den Commit ab.\n#\n# Auf Branch main\n# Ihr Branch ist auf demselben Stand wie \'origin/main\'.\n#\n# Zum Commit vorgemerkte \u{e4}nderungen:\n#\tneue Datei:     file\n#"))
        ];
        let commit = CommitMessage::from_fragments(ast, None);

        assert_eq!(commit.insert_after_last_full_body(vec![Fragment::Body(Body::from("Relates-to: #656"))]).get_ast(), vec![
            Fragment::Body(Body::from("Add file")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("Looks-like-a-trailer: But isn\'t")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("This adds file primarily for demonstration purposes. It might not be\nuseful as an actual commit, but it\'s very useful as a example to use in\ntests.")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("Relates-to: #128\nRelates-to: #656")),
            Fragment::Body(Body::default()),
            Fragment::Comment(Comment::from("# Short (50 chars or less) summary of changes\n#\n# More detailed explanatory text, if necessary.  Wrap it to\n# about 72 characters or so.  In some contexts, the first\n# line is treated as the subject of an email and the rest of\n# the text as the body.  The blank line separating the\n# summary from the body is critical (unless you omit the body\n# entirely); tools like rebase can get confused if you run\n# the two together.\n#\n# Further paragraphs come after blank lines.\n#\n#   - Bullet points are okay, too\n#\n#   - Typically a hyphen or asterisk is used for the bullet,\n#     preceded by a single space, with blank lines in\n#     between, but conventions vary here")),
            Fragment::Body(Body::default()),
            Fragment::Comment(Comment::from("# Bitte geben Sie eine Commit-Beschreibung f\u{fc}r Ihre \u{e4}nderungen ein. Zeilen,\n# die mit \'#\' beginnen, werden ignoriert, und eine leere Beschreibung\n# bricht den Commit ab.\n#\n# Auf Branch main\n# Ihr Branch ist auf demselben Stand wie \'origin/main\'.\n#\n# Zum Commit vorgemerkte \u{e4}nderungen:\n#\tneue Datei:     file\n#"))
        ])
    }

    #[test]
    fn insert_after_last_body_with_no_body() {
        let ast: Vec<Fragment> = vec![
            Fragment::Comment(Comment::from("# Short (50 chars or less) summary of changes\n#\n# More detailed explanatory text, if necessary.  Wrap it to\n# about 72 characters or so.  In some contexts, the first\n# line is treated as the subject of an email and the rest of\n# the text as the body.  The blank line separating the\n# summary from the body is critical (unless you omit the body\n# entirely); tools like rebase can get confused if you run\n# the two together.\n#\n# Further paragraphs come after blank lines.\n#\n#   - Bullet points are okay, too\n#\n#   - Typically a hyphen or asterisk is used for the bullet,\n#     preceded by a single space, with blank lines in\n#     between, but conventions vary here")),
            Fragment::Body(Body::default()),
            Fragment::Comment(Comment::from("# Bitte geben Sie eine Commit-Beschreibung f\u{fc}r Ihre \u{e4}nderungen ein. Zeilen,\n# die mit \'#\' beginnen, werden ignoriert, und eine leere Beschreibung\n# bricht den Commit ab.\n#\n# Auf Branch main\n# Ihr Branch ist auf demselben Stand wie \'origin/main\'.\n#\n# Zum Commit vorgemerkte \u{e4}nderungen:\n#\tneue Datei:     file\n#"))
        ];
        let commit = CommitMessage::from_fragments(ast, None);

        assert_eq!(commit.insert_after_last_full_body(vec![Fragment::Body(Body::from("Relates-to: #656"))]).get_ast(), vec![
            Fragment::Body(Body::from("Relates-to: #656")),
            Fragment::Comment(Comment::from("# Short (50 chars or less) summary of changes\n#\n# More detailed explanatory text, if necessary.  Wrap it to\n# about 72 characters or so.  In some contexts, the first\n# line is treated as the subject of an email and the rest of\n# the text as the body.  The blank line separating the\n# summary from the body is critical (unless you omit the body\n# entirely); tools like rebase can get confused if you run\n# the two together.\n#\n# Further paragraphs come after blank lines.\n#\n#   - Bullet points are okay, too\n#\n#   - Typically a hyphen or asterisk is used for the bullet,\n#     preceded by a single space, with blank lines in\n#     between, but conventions vary here")),
            Fragment::Body(Body::default()),
            Fragment::Comment(Comment::from("# Bitte geben Sie eine Commit-Beschreibung f\u{fc}r Ihre \u{e4}nderungen ein. Zeilen,\n# die mit \'#\' beginnen, werden ignoriert, und eine leere Beschreibung\n# bricht den Commit ab.\n#\n# Auf Branch main\n# Ihr Branch ist auf demselben Stand wie \'origin/main\'.\n#\n# Zum Commit vorgemerkte \u{e4}nderungen:\n#\tneue Datei:     file\n#"))
        ])
    }

    #[test]
    fn can_get_subject_from_commit_with_all_features() {
        let message = CommitMessage::from(COMMIT_WITH_ALL_FEATURES);

        assert_eq!(message.get_subject(), Subject::from("Add file"))
    }

    #[test]
    fn can_get_body_from_commit_with_all_features() {
        let message = CommitMessage::from(COMMIT_WITH_ALL_FEATURES);

        assert_eq!(
            message.get_body(),
            Bodies::from(vec![
                Body::default(),
                Body::from("Looks-like-a-trailer: But isn't"),
                Body::default(),
                Body::from(indoc!(
                    "
                    This adds file primarily for demonstration purposes. It might not be
                    useful as an actual commit, but it's very useful as a example to use in
                    tests."
                ))
            ])
        )
    }

    #[test]
    fn can_get_scissors_section_from_commit_with_all_features() {
        let message = CommitMessage::from(COMMIT_WITH_ALL_FEATURES);

        assert_eq!(
            message.get_scissors(),
            Some(Scissors::from(indoc!(
                "
                # ------------------------ >8 ------------------------
                # \u{00E4}ndern oder entfernen Sie nicht die obige Zeile.
                # Alles unterhalb von ihr wird ignoriert.
                diff --git a/file b/file
                new file mode 100644
                index 0000000..e69de29
                "
            )))
        )
    }

    #[test]
    fn can_get_comments_from_commit_with_all_features() {
        let message = CommitMessage::from(COMMIT_WITH_ALL_FEATURES);

        assert_eq!(
            message.get_comments(),
            Comments::from(vec![
                Comment::from(indoc!(
                    "
                    # Short (50 chars or less) summary of changes
                    #
                    # More detailed explanatory text, if necessary.  Wrap it to
                    # about 72 characters or so.  In some contexts, the first
                    # line is treated as the subject of an email and the rest of
                    # the text as the body.  The blank line separating the
                    # summary from the body is critical (unless you omit the body
                    # entirely); tools like rebase can get confused if you run
                    # the two together.
                    #
                    # Further paragraphs come after blank lines.
                    #
                    #   - Bullet points are okay, too
                    #
                    #   - Typically a hyphen or asterisk is used for the bullet,
                    #     preceded by a single space, with blank lines in
                    #     between, but conventions vary here"
                )),
                Comment::from(indoc!(
                    "
                    # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
                    # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
                    # bricht den Commit ab.
                    #
                    # Auf Branch main
                    # Ihr Branch ist auf demselben Stand wie 'origin/main'.
                    #
                    # Zum Commit vorgemerkte \u{00E4}nderungen:
                    #	neue Datei:     file
                    #"
                ))
            ])
        )
    }

    #[test]
    fn can_get_trailers_from_commit_with_all_features() {
        let message = CommitMessage::from(COMMIT_WITH_ALL_FEATURES);

        assert_eq!(
            message.get_trailers(),
            Trailers::from(vec![Trailer::new("Relates-to", "#128")])
        )
    }

    const LONG_SUBJECT_ONLY_COMMIT: &str = indoc!(
        "
        Initial Commit
        # Short (50 chars or less) summary of changes
        #
        # More detailed explanatory text, if necessary.  Wrap it to
        # about 72 characters or so.  In some contexts, the first
        # line is treated as the subject of an email and the rest of
        # the text as the body.  The blank line separating the
        # summary from the body is critical (unless you omit the body
        # entirely); tools like rebase can get confused if you run
        # the two together.
        #
        # Further paragraphs come after blank lines.
        #
        #   - Bullet points are okay, too
        #
        #   - Typically a hyphen or asterisk is used for the bullet,
        #     preceded by a single space, with blank lines in
        #     between, but conventions vary here

        # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
        # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
        # bricht den Commit ab.
        #
        # Auf Branch master
        #
        # Initialer Commit
        #
        # Zum Commit vorgemerkte \u{00E4}nderungen:
        #	neue Datei:     src/bodies.rs
        #	neue Datei:     src/body.rs
        #	neue Datei:     src/comment.rs
        #	neue Datei:     src/comments.rs
        #	neue Datei:     src/commit_message.rs
        #	neue Datei:     src/scissors.rs
        #	neue Datei:     src/subject.rs
        #	neue Datei:     src/trailer.rs
        #	neue Datei:     src/trailers.rs
        #
        # \u{00E4}nderungen, die nicht zum Commit vorgemerkt sind:
        #	ge\u{00E4}ndert:       src/bodies.rs
        #	ge\u{00E4}ndert:       src/body.rs
        #	ge\u{00E4}ndert:       src/comment.rs
        #	ge\u{00E4}ndert:       src/comments.rs
        #	ge\u{00E4}ndert:       src/commit_message.rs
        #	ge\u{00E4}ndert:       src/scissors.rs
        #	ge\u{00E4}ndert:       src/subject.rs
        #	ge\u{00E4}ndert:       src/trailer.rs
        #	ge\u{00E4}ndert:       src/trailers.rs
        #
        # Unversionierte Dateien:
        #	.gitignore
        #	Cargo.toml
        #	src/lib.rs
        #
        # ------------------------ >8 ------------------------
        # \u{00E4}ndern oder entfernen Sie nicht die obige Zeile.
        # Alles unterhalb von ihr wird ignoriert.
        diff --git a/src/bodies.rs b/src/bodies.rs
        new file mode 100644
        index 0000000..e69de29
        diff --git a/src/body.rs b/src/body.rs
        new file mode 100644
        index 0000000..e69de29
        diff --git a/src/comment.rs b/src/comment.rs
        new file mode 100644
        index 0000000..e69de29
        diff --git a/src/comments.rs b/src/comments.rs
        new file mode 100644
        index 0000000..e69de29
        diff --git a/src/commit_message.rs b/src/commit_message.rs
        new file mode 100644
        index 0000000..e69de29
        diff --git a/src/scissors.rs b/src/scissors.rs
        new file mode 100644
        index 0000000..e69de29
        diff --git a/src/subject.rs b/src/subject.rs
        new file mode 100644
        index 0000000..e69de29
        diff --git a/src/trailer.rs b/src/trailer.rs
        new file mode 100644
        index 0000000..e69de29
        diff --git a/src/trailers.rs b/src/trailers.rs
        new file mode 100644
        index 0000000..e69de29"
    );

    #[test]
    fn can_reliably_parse_from_subject_only_commit() {
        let first_commit_message = CommitMessage::from(LONG_SUBJECT_ONLY_COMMIT);
        let string_version_of_commit = String::from(first_commit_message.clone());
        let second_commit_message = CommitMessage::from(string_version_of_commit.clone());

        assert_eq!(string_version_of_commit, LONG_SUBJECT_ONLY_COMMIT);
        assert_eq!(first_commit_message, second_commit_message)
    }

    #[test]
    fn can_get_ast_from_subject_only_commit() {
        let message = CommitMessage::from(LONG_SUBJECT_ONLY_COMMIT);
        let ast: Vec<Fragment> = vec![
            Fragment::Body(Body::from("Initial Commit")),
            Fragment::Comment(Comment::from("# Short (50 chars or less) summary of changes\n#\n# More detailed explanatory text, if necessary.  Wrap it to\n# about 72 characters or so.  In some contexts, the first\n# line is treated as the subject of an email and the rest of\n# the text as the body.  The blank line separating the\n# summary from the body is critical (unless you omit the body\n# entirely); tools like rebase can get confused if you run\n# the two together.\n#\n# Further paragraphs come after blank lines.\n#\n#   - Bullet points are okay, too\n#\n#   - Typically a hyphen or asterisk is used for the bullet,\n#     preceded by a single space, with blank lines in\n#     between, but conventions vary here")),
            Fragment::Body(Body::default()),
            Fragment::Comment(Comment::from("# Bitte geben Sie eine Commit-Beschreibung f\u{fc}r Ihre \u{e4}nderungen ein. Zeilen,\n# die mit \'#\' beginnen, werden ignoriert, und eine leere Beschreibung\n# bricht den Commit ab.\n#\n# Auf Branch master\n#\n# Initialer Commit\n#\n# Zum Commit vorgemerkte \u{e4}nderungen:\n#\tneue Datei:     src/bodies.rs\n#\tneue Datei:     src/body.rs\n#\tneue Datei:     src/comment.rs\n#\tneue Datei:     src/comments.rs\n#\tneue Datei:     src/commit_message.rs\n#\tneue Datei:     src/scissors.rs\n#\tneue Datei:     src/subject.rs\n#\tneue Datei:     src/trailer.rs\n#\tneue Datei:     src/trailers.rs\n#\n# \u{e4}nderungen, die nicht zum Commit vorgemerkt sind:\n#\tge\u{e4}ndert:       src/bodies.rs\n#\tge\u{e4}ndert:       src/body.rs\n#\tge\u{e4}ndert:       src/comment.rs\n#\tge\u{e4}ndert:       src/comments.rs\n#\tge\u{e4}ndert:       src/commit_message.rs\n#\tge\u{e4}ndert:       src/scissors.rs\n#\tge\u{e4}ndert:       src/subject.rs\n#\tge\u{e4}ndert:       src/trailer.rs\n#\tge\u{e4}ndert:       src/trailers.rs\n#\n# Unversionierte Dateien:\n#\t.gitignore\n#\tCargo.toml\n#\tsrc/lib.rs\n#")),
        ];

        assert_eq!(message.get_ast(), ast)
    }

    #[test]
    fn can_get_subject_from_subject_only_commit() {
        let message = CommitMessage::from(LONG_SUBJECT_ONLY_COMMIT);

        assert_eq!(message.get_subject(), Subject::from("Initial Commit"))
    }

    #[test]
    fn can_get_body_from_subject_only_commit() {
        let message = CommitMessage::from(LONG_SUBJECT_ONLY_COMMIT);
        let bodies: Vec<Body> = vec![];

        assert_eq!(message.get_body(), Bodies::from(bodies))
    }

    #[test]
    fn can_get_scissors_section_from_subject_only_commit() {
        let message = CommitMessage::from(LONG_SUBJECT_ONLY_COMMIT);

        assert_eq!(
            message.get_scissors(),
            Some(Scissors::from(indoc!(
                "
                # ------------------------ >8 ------------------------
                # \u{00E4}ndern oder entfernen Sie nicht die obige Zeile.
                # Alles unterhalb von ihr wird ignoriert.
                diff --git a/src/bodies.rs b/src/bodies.rs
                new file mode 100644
                index 0000000..e69de29
                diff --git a/src/body.rs b/src/body.rs
                new file mode 100644
                index 0000000..e69de29
                diff --git a/src/comment.rs b/src/comment.rs
                new file mode 100644
                index 0000000..e69de29
                diff --git a/src/comments.rs b/src/comments.rs
                new file mode 100644
                index 0000000..e69de29
                diff --git a/src/commit_message.rs b/src/commit_message.rs
                new file mode 100644
                index 0000000..e69de29
                diff --git a/src/scissors.rs b/src/scissors.rs
                new file mode 100644
                index 0000000..e69de29
                diff --git a/src/subject.rs b/src/subject.rs
                new file mode 100644
                index 0000000..e69de29
                diff --git a/src/trailer.rs b/src/trailer.rs
                new file mode 100644
                index 0000000..e69de29
                diff --git a/src/trailers.rs b/src/trailers.rs
                new file mode 100644
                index 0000000..e69de29"
            )))
        )
    }

    #[test]
    fn can_get_comments_from_subject_only_commit() {
        let message = CommitMessage::from(LONG_SUBJECT_ONLY_COMMIT);

        assert_eq!(
            message.get_comments(),
            Comments::from(vec![
                Comment::from(indoc!(
                    "
                    # Short (50 chars or less) summary of changes
                    #
                    # More detailed explanatory text, if necessary.  Wrap it to
                    # about 72 characters or so.  In some contexts, the first
                    # line is treated as the subject of an email and the rest of
                    # the text as the body.  The blank line separating the
                    # summary from the body is critical (unless you omit the body
                    # entirely); tools like rebase can get confused if you run
                    # the two together.
                    #
                    # Further paragraphs come after blank lines.
                    #
                    #   - Bullet points are okay, too
                    #
                    #   - Typically a hyphen or asterisk is used for the bullet,
                    #     preceded by a single space, with blank lines in
                    #     between, but conventions vary here"
                )),
                Comment::from(indoc!(
                    "
                    # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
                    # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
                    # bricht den Commit ab.
                    #
                    # Auf Branch master
                    #
                    # Initialer Commit
                    #
                    # Zum Commit vorgemerkte \u{00E4}nderungen:
                    #	neue Datei:     src/bodies.rs
                    #	neue Datei:     src/body.rs
                    #	neue Datei:     src/comment.rs
                    #	neue Datei:     src/comments.rs
                    #	neue Datei:     src/commit_message.rs
                    #	neue Datei:     src/scissors.rs
                    #	neue Datei:     src/subject.rs
                    #	neue Datei:     src/trailer.rs
                    #	neue Datei:     src/trailers.rs
                    #
                    # \u{00E4}nderungen, die nicht zum Commit vorgemerkt sind:
                    #	ge\u{00E4}ndert:       src/bodies.rs
                    #	ge\u{00E4}ndert:       src/body.rs
                    #	ge\u{00E4}ndert:       src/comment.rs
                    #	ge\u{00E4}ndert:       src/comments.rs
                    #	ge\u{00E4}ndert:       src/commit_message.rs
                    #	ge\u{00E4}ndert:       src/scissors.rs
                    #	ge\u{00E4}ndert:       src/subject.rs
                    #	ge\u{00E4}ndert:       src/trailer.rs
                    #	ge\u{00E4}ndert:       src/trailers.rs
                    #
                    # Unversionierte Dateien:
                    #	.gitignore
                    #	Cargo.toml
                    #	src/lib.rs
                    #"
                ))
            ])
        )
    }

    #[test]
    fn can_get_trailers_from_subject_only_commit() {
        let message = CommitMessage::from(LONG_SUBJECT_ONLY_COMMIT);
        let trailers: Vec<Trailer> = Vec::default();

        assert_eq!(message.get_trailers(), Trailers::from(trailers))
    }

    const NOT_VERBOSE_COMMIT: &str = indoc!(
        "
        Update bashrc to include kubernetes completions

        This should make it easier to deploy things for the developers.
        Benchmarked with Hyperfine, no noticable performance decrease.

        # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
        # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
        # bricht den Commit ab.
        #
        # Datum:            Sat Jun 27 21:40:14 2020 +0200
        #
        # Auf Branch master
        #
        # Initialer Commit
        #
        # Zum Commit vorgemerkte \u{00E4}nderungen:
        #	neue Datei:     .bashrc
        #"
    );

    #[test]
    fn can_reliably_parse_from_not_verbose_commit() {
        let first_commit_message = CommitMessage::from(NOT_VERBOSE_COMMIT);
        let string_version_of_commit = String::from(first_commit_message.clone());
        let second_commit_message = CommitMessage::from(string_version_of_commit.clone());

        assert_eq!(string_version_of_commit, NOT_VERBOSE_COMMIT);
        assert_eq!(first_commit_message, second_commit_message)
    }

    #[test]
    fn can_get_ast_from_not_verbose_commit() {
        let message = CommitMessage::from(NOT_VERBOSE_COMMIT);
        let ast: Vec<Fragment> = vec![
            Fragment::Body(Body::from("Update bashrc to include kubernetes completions")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("This should make it easier to deploy things for the developers.\nBenchmarked with Hyperfine, no noticable performance decrease.")),
            Fragment::Body(Body::default()),
            Fragment::Comment(Comment::from("# Bitte geben Sie eine Commit-Beschreibung f\u{fc}r Ihre \u{e4}nderungen ein. Zeilen,\n# die mit \'#\' beginnen, werden ignoriert, und eine leere Beschreibung\n# bricht den Commit ab.\n#\n# Datum:            Sat Jun 27 21:40:14 2020 +0200\n#\n# Auf Branch master\n#\n# Initialer Commit\n#\n# Zum Commit vorgemerkte \u{e4}nderungen:\n#\tneue Datei:     .bashrc\n#"))
        ];

        assert_eq!(message.get_ast(), ast)
    }

    #[test]
    fn can_get_subject_from_not_verbose_commit() {
        let message = CommitMessage::from(NOT_VERBOSE_COMMIT);

        assert_eq!(
            message.get_subject(),
            Subject::from("Update bashrc to include kubernetes completions")
        )
    }

    #[test]
    fn can_get_body_from_not_verbose_commit() {
        let message = CommitMessage::from(NOT_VERBOSE_COMMIT);

        assert_eq!(
            message.get_body(),
            Bodies::from(vec![
                Body::default(),
                Body::from(indoc!(
                    "
                    This should make it easier to deploy things for the developers.
                    Benchmarked with Hyperfine, no noticable performance decrease."
                )),
            ])
        )
    }

    #[test]
    fn can_get_scissors_section_from_not_verbose_commit() {
        let message = CommitMessage::from(NOT_VERBOSE_COMMIT);

        assert_eq!(message.get_scissors(), None)
    }

    #[test]
    fn can_get_comments_from_not_verbose_commit() {
        let message = CommitMessage::from(NOT_VERBOSE_COMMIT);

        assert_eq!(
            message.get_comments(),
            Comments::from(vec![
                Comment::from(indoc!(
                    "
                    # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
                    # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
                    # bricht den Commit ab.
                    #
                    # Datum:            Sat Jun 27 21:40:14 2020 +0200
                    #
                    # Auf Branch master
                    #
                    # Initialer Commit
                    #
                    # Zum Commit vorgemerkte \u{00E4}nderungen:
                    #	neue Datei:     .bashrc
                    #"
                ))
            ])
        )
    }

    #[test]
    fn can_get_trailers_from_not_verbose_commit() {
        let message = CommitMessage::from(NOT_VERBOSE_COMMIT);
        let trailers: Vec<Trailer> = Vec::default();

        assert_eq!(message.get_trailers(), Trailers::from(trailers))
    }

    const NON_STANDARD_COMMENT_CHARACTER: &str = indoc!(
        "
        Allow the server to respond to https

        This allows the server to respond to HTTPS requests, by correcting the port binding.
        We should see a nice speed increase from this

        fixes:
        #6436
        #6437
        #6438

        ; Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00C4}nderungen ein. Zeilen,
        ; die mit ';' beginnen, werden ignoriert, und eine leere Beschreibung
        ; bricht den Commit ab."
    );

    #[test]
    fn can_reliably_parse_from_non_standard_comment_char_commit() {
        let first_commit_message = CommitMessage::from(NON_STANDARD_COMMENT_CHARACTER);
        let string_version_of_commit = String::from(first_commit_message.clone());
        let second_commit_message = CommitMessage::from(string_version_of_commit.clone());

        assert_eq!(string_version_of_commit, NON_STANDARD_COMMENT_CHARACTER);
        assert_eq!(first_commit_message, second_commit_message)
    }

    #[test]
    fn can_get_ast_from_non_standard_comment_char_commit() {
        let message = CommitMessage::from(NON_STANDARD_COMMENT_CHARACTER);
        let ast: Vec<Fragment> = vec![
            Fragment::Body(Body::from("Allow the server to respond to https")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("This allows the server to respond to HTTPS requests, by correcting the port binding.\nWe should see a nice speed increase from this")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("fixes:\n#6436\n#6437\n#6438")),
            Fragment::Body(Body::default()),
            Fragment::Comment(Comment::from("; Bitte geben Sie eine Commit-Beschreibung f\u{fc}r Ihre \u{c4}nderungen ein. Zeilen,\n; die mit \';\' beginnen, werden ignoriert, und eine leere Beschreibung\n; bricht den Commit ab."))
        ];

        assert_eq!(message.get_ast(), ast)
    }

    #[test]
    fn can_get_subject_from_non_standard_comment_char_commit() {
        let message = CommitMessage::from(NON_STANDARD_COMMENT_CHARACTER);

        assert_eq!(
            message.get_subject(),
            Subject::from("Allow the server to respond to https")
        )
    }

    #[test]
    fn can_get_body_from_non_standard_comment_char_commit() {
        let message = CommitMessage::from(NON_STANDARD_COMMENT_CHARACTER);

        assert_eq!(
            message.get_body(),
            Bodies::from(vec![
                Body::default(),
                Body::from(indoc!(
                    "
                    This allows the server to respond to HTTPS requests, by correcting the port binding.
                    We should see a nice speed increase from this"
                    )),
                Body::default(),
                Body::from(indoc!(
                    "
                    fixes:
                    #6436
                    #6437
                    #6438"
                )),
            ])
        )
    }

    #[test]
    fn can_get_scissors_section_from_non_standard_comment_char_commit() {
        let message = CommitMessage::from(NON_STANDARD_COMMENT_CHARACTER);

        assert_eq!(message.get_scissors(), None)
    }

    #[test]
    fn can_get_comments_from_non_standard_comment_char_commit() {
        let message = CommitMessage::from(NON_STANDARD_COMMENT_CHARACTER);

        assert_eq!(
            message.get_comments(),
            Comments::from(vec![Comment::from(indoc!(
                "
                ; Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00C4}nderungen ein. Zeilen,
                ; die mit ';' beginnen, werden ignoriert, und eine leere Beschreibung
                ; bricht den Commit ab."
            ))])
        )
    }

    #[test]
    fn can_get_trailers_from_non_standard_comment_char_commit() {
        let message = CommitMessage::from(NON_STANDARD_COMMENT_CHARACTER);
        let trailers: Vec<Trailer> = Vec::default();

        assert_eq!(message.get_trailers(), Trailers::from(trailers))
    }

    const MULTIPLE_TRAILERS: &str = indoc!(
        "
        Update bashrc to include kubernetes completions

        This should make it easier to deploy things for the developers.
        Benchmarked with Hyperfine, no noticable performance decrease.

        Co-authored-by: Billie Thomposon <billie@example.com>
        Co-authored-by: Somebody Else <somebody@example.com>

        # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
        # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
        # bricht den Commit ab.
        #
        # Datum:            Sat Jun 27 21:40:14 2020 +0200
        #
        # Auf Branch master
        #
        # Initialer Commit
        #
        # Zum Commit vorgemerkte \u{00E4}nderungen:
        #	neue Datei:     .bashrc
        #"
    );

    #[test]
    fn can_reliably_parse_from_multiple_trailers() {
        let first_commit_message = CommitMessage::from(MULTIPLE_TRAILERS);
        let string_version_of_commit = String::from(first_commit_message.clone());
        let second_commit_message = CommitMessage::from(string_version_of_commit.clone());

        assert_eq!(string_version_of_commit, MULTIPLE_TRAILERS);
        assert_eq!(first_commit_message, second_commit_message)
    }

    #[test]
    fn can_get_ast_from_multiple_trailers() {
        let message = CommitMessage::from(MULTIPLE_TRAILERS);
        let ast: Vec<Fragment> = vec![
            Fragment::Body(Body::from("Update bashrc to include kubernetes completions")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("This should make it easier to deploy things for the developers.\nBenchmarked with Hyperfine, no noticable performance decrease.")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("Co-authored-by: Billie Thomposon <billie@example.com>\nCo-authored-by: Somebody Else <somebody@example.com>")),
            Fragment::Body(Body::default()),
            Fragment::Comment(Comment::from("# Bitte geben Sie eine Commit-Beschreibung f\u{fc}r Ihre \u{e4}nderungen ein. Zeilen,\n# die mit \'#\' beginnen, werden ignoriert, und eine leere Beschreibung\n# bricht den Commit ab.\n#\n# Datum:            Sat Jun 27 21:40:14 2020 +0200\n#\n# Auf Branch master\n#\n# Initialer Commit\n#\n# Zum Commit vorgemerkte \u{e4}nderungen:\n#\tneue Datei:     .bashrc\n#"))
        ];

        assert_eq!(message.get_ast(), ast)
    }

    #[test]
    fn can_get_subject_from_multiple_trailers() {
        let message = CommitMessage::from(MULTIPLE_TRAILERS);

        assert_eq!(
            message.get_subject(),
            Subject::from("Update bashrc to include kubernetes completions")
        )
    }

    #[test]
    fn can_get_body_from_multiple_trailers() {
        let message = CommitMessage::from(MULTIPLE_TRAILERS);

        assert_eq!(
            message.get_body(),
            Bodies::from(vec![
                Body::default(),
                Body::from(indoc!(
                    "
                    This should make it easier to deploy things for the developers.
                    Benchmarked with Hyperfine, no noticable performance decrease."
                )),
            ])
        )
    }

    #[test]
    fn can_get_scissors_section_from_multiple_trailers() {
        let message = CommitMessage::from(MULTIPLE_TRAILERS);

        assert_eq!(message.get_scissors(), None)
    }

    #[test]
    fn can_get_comments_from_multiple_trailers() {
        let message = CommitMessage::from(MULTIPLE_TRAILERS);

        assert_eq!(
            message.get_comments(),
            Comments::from(vec![
                Comment::from(indoc!(
                    "
                    # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
                    # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
                    # bricht den Commit ab.
                    #
                    # Datum:            Sat Jun 27 21:40:14 2020 +0200
                    #
                    # Auf Branch master
                    #
                    # Initialer Commit
                    #
                    # Zum Commit vorgemerkte \u{00E4}nderungen:
                    #	neue Datei:     .bashrc
                    #"
                ))
            ])
        )
    }

    #[test]
    fn can_get_trailers_from_multiple_trailers() {
        let message = CommitMessage::from(MULTIPLE_TRAILERS);
        let trailers: Vec<Trailer> = vec![
            Trailer::new("Co-authored-by", "Billie Thomposon <billie@example.com>"),
            Trailer::new("Co-authored-by", "Somebody Else <somebody@example.com>"),
        ];

        assert_eq!(message.get_trailers(), Trailers::from(trailers))
    }

    const TRAILING_EMPTY_NEWLINES: &str = indoc!(
        "
        Update bashrc to include kubernetes completions

        This should make it easier to deploy things for the developers.
        Benchmarked with Hyperfine, no noticable performance decrease.

        Co-authored-by: Billie Thomposon <billie@example.com>
        Co-authored-by: Somebody Else <somebody@example.com>

        # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
        # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
        # bricht den Commit ab.
        #
        # Datum:            Sat Jun 27 21:40:14 2020 +0200
        #
        # Auf Branch master
        #
        # Initialer Commit
        #
        # Zum Commit vorgemerkte \u{00E4}nderungen:
        #	neue Datei:     .bashrc
        #


        "
    );

    #[test]
    fn can_reliably_parse_from_trailing_empty_newlines() {
        let first_commit_message = CommitMessage::from(TRAILING_EMPTY_NEWLINES);
        let string_version_of_commit = String::from(first_commit_message.clone());
        let second_commit_message = CommitMessage::from(string_version_of_commit.clone());

        assert_eq!(string_version_of_commit, TRAILING_EMPTY_NEWLINES);
        assert_eq!(first_commit_message, second_commit_message)
    }

    #[test]
    fn can_get_ast_from_trailing_empty_newlines() {
        let message = CommitMessage::from(TRAILING_EMPTY_NEWLINES);
        let ast: Vec<Fragment> = vec![
            Fragment::Body(Body::from("Update bashrc to include kubernetes completions")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("This should make it easier to deploy things for the developers.\nBenchmarked with Hyperfine, no noticable performance decrease.")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("Co-authored-by: Billie Thomposon <billie@example.com>\nCo-authored-by: Somebody Else <somebody@example.com>")),
            Fragment::Body(Body::default()),
            Fragment::Comment(Comment::from("# Bitte geben Sie eine Commit-Beschreibung f\u{fc}r Ihre \u{e4}nderungen ein. Zeilen,\n# die mit \'#\' beginnen, werden ignoriert, und eine leere Beschreibung\n# bricht den Commit ab.\n#\n# Datum:            Sat Jun 27 21:40:14 2020 +0200\n#\n# Auf Branch master\n#\n# Initialer Commit\n#\n# Zum Commit vorgemerkte \u{e4}nderungen:\n#\tneue Datei:     .bashrc\n#")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::default()),
        ];

        assert_eq!(message.get_ast(), ast)
    }

    #[test]
    fn can_get_subject_from_trailing_empty_newlines() {
        let message = CommitMessage::from(TRAILING_EMPTY_NEWLINES);

        assert_eq!(
            message.get_subject(),
            Subject::from("Update bashrc to include kubernetes completions")
        )
    }

    #[test]
    fn can_get_body_from_trailing_empty_newlines() {
        let message = CommitMessage::from(TRAILING_EMPTY_NEWLINES);

        assert_eq!(
            message.get_body(),
            Bodies::from(vec![
                Body::default(),
                Body::from(indoc!(
                    "
                    This should make it easier to deploy things for the developers.
                    Benchmarked with Hyperfine, no noticable performance decrease."
                )),
            ])
        )
    }

    #[test]
    fn can_get_scissors_section_from_trailing_empty_newlines() {
        let message = CommitMessage::from(TRAILING_EMPTY_NEWLINES);

        assert_eq!(message.get_scissors(), None)
    }

    #[test]
    fn can_get_comments_from_trailing_empty_newlines() {
        let message = CommitMessage::from(TRAILING_EMPTY_NEWLINES);

        assert_eq!(
            message.get_comments(),
            Comments::from(vec![
                Comment::from(indoc!(
                    "
                    # Bitte geben Sie eine Commit-Beschreibung f\u{00FC}r Ihre \u{00E4}nderungen ein. Zeilen,
                    # die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
                    # bricht den Commit ab.
                    #
                    # Datum:            Sat Jun 27 21:40:14 2020 +0200
                    #
                    # Auf Branch master
                    #
                    # Initialer Commit
                    #
                    # Zum Commit vorgemerkte \u{00E4}nderungen:
                    #	neue Datei:     .bashrc
                    #"
                ))
            ])
        )
    }

    #[test]
    fn can_get_trailers_from_trailing_empty_newlines() {
        let message = CommitMessage::from(TRAILING_EMPTY_NEWLINES);
        let trailers: Vec<Trailer> = vec![
            Trailer::new("Co-authored-by", "Billie Thomposon <billie@example.com>"),
            Trailer::new("Co-authored-by", "Somebody Else <somebody@example.com>"),
        ];

        assert_eq!(message.get_trailers(), Trailers::from(trailers))
    }

    const COMMIT_MESSAGE_WITH_NO_COMMENTS: &str = indoc!(
        "
        Update bashrc to include kubernetes completions

        This should make it easier to deploy things for the developers.
        Benchmarked with Hyperfine, no noticable performance decrease.

        Co-authored-by: Billie Thomposon <billie@example.com>
        Co-authored-by: Somebody Else <somebody@example.com>
        "
    );

    #[test]
    fn can_reliably_parse_from_a_commit_message_without_commits() {
        let first_commit_message = CommitMessage::from(COMMIT_MESSAGE_WITH_NO_COMMENTS);
        let string_version_of_commit = String::from(first_commit_message.clone());
        let second_commit_message = CommitMessage::from(string_version_of_commit.clone());

        assert_eq!(string_version_of_commit, COMMIT_MESSAGE_WITH_NO_COMMENTS);
        assert_eq!(first_commit_message, second_commit_message)
    }

    #[test]
    fn can_get_ast_from_a_commit_message_without_commits() {
        let message = CommitMessage::from(COMMIT_MESSAGE_WITH_NO_COMMENTS);
        let ast: Vec<Fragment> = vec![
            Fragment::Body(Body::from("Update bashrc to include kubernetes completions")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("This should make it easier to deploy things for the developers.\nBenchmarked with Hyperfine, no noticable performance decrease.")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("Co-authored-by: Billie Thomposon <billie@example.com>\nCo-authored-by: Somebody Else <somebody@example.com>")),
            Fragment::Body(Body::default()),
        ];

        assert_eq!(message.get_ast(), ast)
    }

    #[test]
    fn can_get_subject_from_a_commit_message_without_commits() {
        let message = CommitMessage::from(COMMIT_MESSAGE_WITH_NO_COMMENTS);

        assert_eq!(
            message.get_subject(),
            Subject::from("Update bashrc to include kubernetes completions")
        )
    }

    #[test]
    fn can_get_body_from_a_commit_message_without_commits() {
        let message = CommitMessage::from(COMMIT_MESSAGE_WITH_NO_COMMENTS);

        assert_eq!(
            message.get_body(),
            Bodies::from(vec![
                Body::default(),
                Body::from(indoc!(
                    "
                    This should make it easier to deploy things for the developers.
                    Benchmarked with Hyperfine, no noticable performance decrease."
                )),
            ])
        )
    }

    #[test]
    fn can_get_scissors_section_from_a_commit_message_without_commits() {
        let message = CommitMessage::from(COMMIT_MESSAGE_WITH_NO_COMMENTS);

        assert_eq!(message.get_scissors(), None)
    }

    #[test]
    fn can_get_comments_from_a_commit_message_without_commits() {
        let message = CommitMessage::from(COMMIT_MESSAGE_WITH_NO_COMMENTS);
        let comments: Vec<Comment> = vec![];

        assert_eq!(message.get_comments(), Comments::from(comments))
    }

    #[test]
    fn can_get_trailers_from_a_commit_message_without_commits() {
        let message = CommitMessage::from(COMMIT_MESSAGE_WITH_NO_COMMENTS);
        let trailers: Vec<Trailer> = vec![
            Trailer::new("Co-authored-by", "Billie Thomposon <billie@example.com>"),
            Trailer::new("Co-authored-by", "Somebody Else <somebody@example.com>"),
        ];

        assert_eq!(message.get_trailers(), Trailers::from(trailers))
    }

    const COMMIT_MESSAGE_WITH_WHITESPACE_STARTING_LAST_LINE: &str = indoc!(
        "
        Update bashrc to include kubernetes completions

        This should make it easier to deploy things for the developers.
        Benchmarked with Hyperfine, no noticable performance decrease.

        Co-authored-by: Billie Thomposon <billie@example.com>
         Co-authored-by: Somebody Else <somebody@example.com>
        "
    );

    #[test]
    fn can_reliably_parse_from_a_commit_message_with_trailing_whitespace() {
        let first_commit_message =
            CommitMessage::from(COMMIT_MESSAGE_WITH_WHITESPACE_STARTING_LAST_LINE);
        let string_version_of_commit = String::from(first_commit_message.clone());
        let second_commit_message = CommitMessage::from(string_version_of_commit.clone());

        assert_eq!(
            string_version_of_commit,
            COMMIT_MESSAGE_WITH_WHITESPACE_STARTING_LAST_LINE
        );
        assert_eq!(first_commit_message, second_commit_message)
    }

    #[test]
    fn can_get_ast_from_a_commit_message_with_trailing_whitespace() {
        let message = CommitMessage::from(COMMIT_MESSAGE_WITH_WHITESPACE_STARTING_LAST_LINE);
        let ast: Vec<Fragment> = vec![
            Fragment::Body(Body::from("Update bashrc to include kubernetes completions")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("This should make it easier to deploy things for the developers.\nBenchmarked with Hyperfine, no noticable performance decrease.")),
            Fragment::Body(Body::default()),
            Fragment::Body(Body::from("Co-authored-by: Billie Thomposon <billie@example.com>\n Co-authored-by: Somebody Else <somebody@example.com>")),
            Fragment::Body(Body::default()),
        ];

        assert_eq!(message.get_ast(), ast)
    }

    #[test]
    fn can_get_subject_from_a_commit_message_with_trailing_whitespace() {
        let message = CommitMessage::from(COMMIT_MESSAGE_WITH_WHITESPACE_STARTING_LAST_LINE);

        assert_eq!(
            message.get_subject(),
            Subject::from("Update bashrc to include kubernetes completions")
        )
    }

    #[test]
    fn can_get_body_from_a_commit_message_with_trailing_whitespace() {
        let message = CommitMessage::from(COMMIT_MESSAGE_WITH_WHITESPACE_STARTING_LAST_LINE);

        assert_eq!(
            message.get_body(),
            Bodies::from(vec![
                Body::default(),
                Body::from(indoc!(
                    "
                    This should make it easier to deploy things for the developers.
                    Benchmarked with Hyperfine, no noticable performance decrease."
                )),
            ])
        )
    }

    #[test]
    fn can_get_scissors_section_from_a_commit_message_with_trailing_whitespace() {
        let message = CommitMessage::from(COMMIT_MESSAGE_WITH_WHITESPACE_STARTING_LAST_LINE);

        assert_eq!(message.get_scissors(), None)
    }

    #[test]
    fn can_get_comments_from_a_commit_message_with_trailing_whitespace() {
        let message = CommitMessage::from(COMMIT_MESSAGE_WITH_WHITESPACE_STARTING_LAST_LINE);
        let comments: Vec<Comment> = vec![];

        assert_eq!(message.get_comments(), Comments::from(comments))
    }

    #[test]
    fn can_get_trailers_from_a_commit_message_with_trailing_whitespace() {
        let message = CommitMessage::from(COMMIT_MESSAGE_WITH_WHITESPACE_STARTING_LAST_LINE);
        let trailers: Vec<Trailer> = vec![
            Trailer::new("Co-authored-by", "Billie Thomposon <billie@example.com>"),
            Trailer::new(" Co-authored-by", "Somebody Else <somebody@example.com>"),
        ];

        assert_eq!(message.get_trailers(), Trailers::from(trailers))
    }
}

lazy_static! {
    static ref NOT_WHITESPACE_RE: Regex = Regex::new("^\\W$").unwrap();
}

impl CommitMessage {
    fn guess_comment_character(rest: &str, scissors: Option<Scissors>) -> Option<char> {
        match scissors {
            Some(scissors) => String::from(scissors).chars().next(),
            None => rest
                .lines()
                .rev()
                .find(|line| !line.trim().is_empty())
                .and_then(|line| line.chars().next())
                .filter(|x| NOT_WHITESPACE_RE.is_match(&x.to_string().trim())),
        }
    }
}