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/******************************************************************************/