Skip to main content

git_harvest/changelog/
configuration.rs

1/*********************** GNU General Public License 3.0 ***********************\
2|                                                                              |
3|  Copyright (C) 2026 Kevin Matthes                                            |
4|                                                                              |
5|  This program is free software: you can redistribute it and/or modify        |
6|  it under the terms of the GNU General Public License as published by        |
7|  the Free Software Foundation, either version 3 of the License, or           |
8|  (at your option) any later version.                                         |
9|                                                                              |
10|  This program is distributed in the hope that it will be useful,             |
11|  but WITHOUT ANY WARRANTY; without even the implied warranty of              |
12|  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               |
13|  GNU General Public License for more details.                                |
14|                                                                              |
15|  You should have received a copy of the GNU General Public License           |
16|  along with this program.  If not, see <https://www.gnu.org/licenses/>.      |
17|                                                                              |
18\******************************************************************************/
19
20//! The harvest configuration, which lives inside the CHANGELOG itself.
21
22/// How a commit subject encodes the bucket a change belongs to.
23#[derive(
24    Clone,
25    Copy,
26    Debug,
27    Default,
28    Eq,
29    PartialEq,
30    serde::Deserialize,
31    serde::Serialize,
32)]
33#[serde(rename_all = "snake_case")]
34pub enum Grammar {
35    /// `Bucket <delimiter> entry`, for example `Added ::= a new option`.
36    #[default]
37    Delimited,
38
39    /// `[Bucket] entry`, for example `[Added] a new option`.
40    Bracketed,
41}
42
43/// The shape the assembled CHANGELOG is rendered in for readers.
44#[derive(
45    Clone,
46    Copy,
47    Debug,
48    Default,
49    Eq,
50    PartialEq,
51    serde::Deserialize,
52    serde::Serialize,
53)]
54#[serde(rename_all = "snake_case")]
55pub enum Renderer {
56    /// A *Keep a Changelog* Markdown document beside the RON source.
57    #[default]
58    Markdown,
59}
60
61/// The harvest configuration.
62///
63/// `git-harvest` has no separate configuration file:  these settings live in
64/// the CHANGELOG document, and [`Configuration::default`] is what
65/// `git-harvest init` writes into a fresh one.
66#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
67pub struct Configuration {
68    /// The token separating a bucket from its entry under
69    /// [`Grammar::Delimited`].
70    pub delimiter: String,
71
72    /// How a commit subject encodes its bucket.
73    pub grammar: Grammar,
74
75    /// The buckets a change may be filed under, in rendering order.
76    pub buckets: Vec<String>,
77
78    /// The bucket for a change whose commit names none, where one is wanted.
79    pub fallback_bucket: Option<String>,
80
81    /// The shape the assembled CHANGELOG is rendered in.
82    pub renderer: Renderer,
83}
84
85impl Configuration {
86    /// Parse a commit subject into a `(bucket, entry text)` pair.
87    ///
88    /// Returns `None` when the subject names no known bucket and no
89    /// `fallback_bucket` is set, or when the entry text is empty.  Under
90    /// [`Grammar::Delimited`] the bucket is what precedes `delimiter`; under
91    /// [`Grammar::Bracketed`] it is what a leading `[..]` encloses.
92    pub fn parse(&self, subject: &str) -> Option<(String, String)> {
93        let named = match self.grammar {
94            Grammar::Delimited => subject.split_once(self.delimiter.as_str()),
95            Grammar::Bracketed => subject
96                .trim_start()
97                .strip_prefix('[')
98                .and_then(|rest| rest.split_once(']')),
99        };
100
101        let (named, text) = named.map_or_else(
102            || (None, subject.trim()),
103            |(a, b)| (Some(a.trim()), b.trim()),
104        );
105
106        if text.is_empty() {
107            return None;
108        }
109
110        named
111            .filter(|bucket| self.buckets.iter().any(|known| known == bucket))
112            .map(str::to_owned)
113            .or_else(|| self.fallback_bucket.clone())
114            .map(|bucket| (bucket, text.to_owned()))
115    }
116}
117
118impl Default for Configuration {
119    /// The defaults `git-harvest init` writes:  the `::=` delimiter, the
120    /// delimited grammar, the six *Keep a Changelog* buckets, no fallback,
121    /// and Markdown rendering.
122    fn default() -> Self {
123        Self {
124            delimiter: "::=".to_owned(),
125            grammar: Grammar::Delimited,
126            buckets: [
127                "Added",
128                "Changed",
129                "Deprecated",
130                "Fixed",
131                "Removed",
132                "Security",
133            ]
134            .iter()
135            .map(|bucket| (*bucket).to_owned())
136            .collect(),
137            fallback_bucket: None,
138            renderer: Renderer::Markdown,
139        }
140    }
141}
142
143/******************************************************************************/