1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! Defines structures that can be used to work with conventional commits.
//! The implementation resembles the v1.0.0 specification defined over at [conventionalcommits.org](https://www.conventionalcommits.org/en/v1.0.0/#specification).

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::{fmt, str::FromStr};

/// The `:<space>` separator.
pub const SEPARATOR_COLON: &str = ": ";

/// The `<space>#` separator for footer notes.
///
/// This type of separator is mostly used when using issues or PR numbers as
/// value.
pub const SEPARATOR_HASHTAG: &str = " #";

/// A commit message.
///
/// As per the specification, a commit message is made out of a mandatory
/// description, an optional body and `0..n` optional footers. The different
/// sections are separated by an empty newline. Footers can be each separated
/// with a newline, however, this is not needed.
///
/// # Example
///
/// ```text
/// feat(some scope): a short and concise description
///
/// This is a longer body message. It can wrapped around
/// and be put onto multiple lines.
///
/// This is still part of the body.
///
/// Fixes #123
/// PR-close #124
/// Signed-off-by: SirWindfield
/// ```
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct Commit<'a> {
    /// The optional body.
    pub body: Option<&'a str>,
    /// The mandatory description.
    pub desc: &'a str,
    /// A list of footers. Empty when none are part of the commit message.
    pub footer: Vec<Footer<'a>>,
    /// Set if the commit is a breaking change.
    pub is_breaking_change: bool,
    /// The optional scope.
    pub scope: Option<&'a str>,
    /// The mandatory type.
    ///
    /// Types other than `feat` and `fix` are optional. For more information, please take a look at the [specification](https://www.conventionalcommits.org/en/v1.0.0/#specification), paragraphs 1-3.
    pub ty: &'a str,
}

impl<'a> Commit<'a> {
    /// Creates a default commit.
    pub fn new() -> Self {
        Self::default()
    }

    /// Creates a commit with the given values.
    pub fn from(
        ty: &'a str,
        scope: Option<&'a str>,
        desc: &'a str,
        body: Option<&'a str>,
        is_breaking_change: bool,
        footer: Vec<Footer<'a>>,
    ) -> Self {
        Self {
            ty,
            scope,
            desc,
            body,
            is_breaking_change,
            footer,
        }
    }
}

/// A commit message footer.
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct Footer<'a> {
    /// The footer word token.
    pub token: &'a str,
    /// The separator.
    pub separator: FooterSeparator,
    /// The footer's value.
    pub value: &'a str,
}

impl<'a> Footer<'a> {
    /// Creates a default footer.
    pub fn new() -> Self {
        Self::default()
    }

    /// Creates a footer with the given values.
    pub fn from(token: &'a str, separator: FooterSeparator, value: &'a str) -> Self {
        Self {
            token,
            separator,
            value,
        }
    }
}

/// The separator used to separate the token and value of a footer.
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub enum FooterSeparator {
    /// The `:<space>` separator is mostly used for values that do not involve
    /// issue or PR references.
    ColonSpace,
    /// The `#<space>` separator is, more often than not, used for issue and PR
    /// references.
    SpaceHashTag,
}

impl Default for FooterSeparator {
    /// Returns the default FooterSeparator, the ColonSpace.
    fn default() -> Self {
        FooterSeparator::ColonSpace
    }
}

impl fmt::Display for FooterSeparator {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FooterSeparator::ColonSpace => write!(f, "{}", SEPARATOR_COLON),
            FooterSeparator::SpaceHashTag => write!(f, "{}", SEPARATOR_HASHTAG),
        }
    }
}

impl FromStr for FooterSeparator {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            SEPARATOR_COLON => Ok(FooterSeparator::ColonSpace),
            SEPARATOR_HASHTAG => Ok(FooterSeparator::SpaceHashTag),
            _ => Err("footer separator not recognized".to_string()),
        }
    }
}