changelog_gen/
config.rs

1use std::fs::File;
2use std::io::Read;
3use std::path::{Path, PathBuf};
4use std::{collections::HashSet, fmt::Display};
5
6use changelog::fmt::SortOptions;
7use clap::{arg, Args, Parser, Subcommand, ValueHint};
8
9use changelog::ser::{Options, OptionsRelease};
10use clap::ValueEnum;
11use indexmap::IndexMap;
12use regex::Regex;
13use semver::Version;
14use serde::{Deserialize, Serialize};
15
16use crate::git_provider::GitProvider;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct MapMessageToSection(pub IndexMap<String, HashSet<String>>);
20
21impl Default for MapMessageToSection {
22    fn default() -> Self {
23        let map = include_str!("../res/map_commit_type_to_section.json");
24        serde_json::de::from_str(map).unwrap()
25    }
26}
27
28impl MapMessageToSection {
29    pub fn to_fmt_options(self) -> changelog::fmt::Options {
30        changelog::fmt::Options {
31            sort_options: SortOptions {
32                section_order: self.0.into_iter().map(|(section, _)| section).collect(),
33                ..Default::default()
34            },
35        }
36    }
37    pub fn into_changelog_ser_options(self) -> Options {
38        Options {
39            release_option: OptionsRelease {
40                ..Default::default()
41            },
42        }
43    }
44
45    pub fn map_section(&self, section: &str) -> Option<String> {
46        let section_normalized = section.to_lowercase();
47
48        for (section, needles) in &self.0 {
49            for needle in needles {
50                let needle_normalized = needle.to_lowercase();
51
52                if section_normalized == needle_normalized {
53                    return Some(section.to_owned());
54                }
55            }
56        }
57
58        None
59    }
60
61    /// Best effort recognition
62    pub fn try_find_section(&self, (message, desc): (&str, &str)) -> Option<String> {
63        let message_normalized = message.to_lowercase();
64        let desc_normalized = desc.to_lowercase();
65
66        for (section, needles) in &self.0 {
67            for needle in needles {
68                let needle_normalized = needle.to_lowercase();
69
70                if message_normalized.contains(&needle_normalized) {
71                    return Some(section.to_owned());
72                }
73                if desc_normalized.contains(&needle_normalized) {
74                    return Some(section.to_owned());
75                }
76            }
77        }
78
79        None
80    }
81
82    pub fn try_new<P: AsRef<Path>>(path: Option<P>) -> anyhow::Result<MapMessageToSection> {
83        match path {
84            Some(path) => {
85                let mut file = File::open(&path)?;
86
87                let mut content = Vec::new();
88
89                file.read_to_end(&mut content)?;
90
91                let map = serde_json::de::from_slice(&content)?;
92                Ok(map)
93            }
94            None => Ok(MapMessageToSection::default()),
95        }
96    }
97}
98
99#[derive(ValueEnum, Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
100pub enum CommitMessageParsing {
101    #[default]
102    Smart,
103    Strict,
104}
105
106// todo: use derive_more::Display when this issue is resolved
107// https://github.com/JelteF/derive_more/issues/216
108impl Display for CommitMessageParsing {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        match self {
111            CommitMessageParsing::Smart => write!(f, "smart"),
112            CommitMessageParsing::Strict => write!(f, "strict"),
113        }
114    }
115}
116
117#[derive(ValueEnum, Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
118pub enum MergeDevVersions {
119    /// Yes if the version is stable, no otherwise
120    #[default]
121    Auto,
122    No,
123    Yes,
124}
125
126impl Display for MergeDevVersions {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        match self {
129            MergeDevVersions::Auto => write!(f, "auto"),
130            MergeDevVersions::No => write!(f, "no"),
131            MergeDevVersions::Yes => write!(f, "yes"),
132        }
133    }
134}
135
136#[derive(Debug, Clone, Parser)]
137#[command(name = "changelog", version, about = "Changelog generator", long_about = None)]
138pub struct Cli {
139    #[command(subcommand)]
140    pub command: Commands,
141}
142
143#[derive(Debug, Clone, Subcommand)]
144pub enum Commands {
145    #[command(alias = "gen")]
146    Generate(Generate),
147    Release(Release),
148    Validate(Validate),
149    Show(Show),
150    New(New),
151    #[command(aliases = ["delete", "rm"])]
152    Remove(Remove),
153}
154
155/// Generate release notes. By default, generate from the last release in the changelog to HEAD.
156#[derive(Debug, Clone, Args)]
157pub struct Generate {
158    /// Path to the changelog file.
159    #[arg(
160        short,
161        long,
162        default_value = "CHANGELOG.md",
163        value_hint = ValueHint::FilePath,
164        short_alias = 'o',
165        alias = "output",
166    )]
167    pub file: Option<PathBuf>,
168    /// Path to the commit type to changelog section map.
169    #[arg(long, value_hint = ValueHint::FilePath)]
170    pub map: Option<PathBuf>,
171    /// Parsing of the commit message.
172    #[arg(long, default_value_t)]
173    pub parsing: CommitMessageParsing,
174    /// Don't include unidentified commits.
175    #[arg(long)]
176    pub exclude_unidentified: bool,
177    /// Don't include commits which are not attached to a pull request.
178    #[arg(long)]
179    pub exclude_not_pr: bool,
180    /// We use the Github api to map commit sha to PRs.
181    #[arg(long, default_value_t)]
182    pub provider: GitProvider,
183    /// Needed for fetching PRs. Example: 'wiiznokes/changelog-generator'. Already defined for you in Github Actions.
184    #[arg(long)]
185    pub repo: Option<String>,
186    /// Omit the PR link from the output.
187    #[arg(long)]
188    pub omit_pr_link: bool,
189    /// Omit contributors' acknowledgements/mention.
190    #[arg(long)]
191    pub omit_thanks: bool,
192    /// Print the result on the standard output.
193    #[arg(long)]
194    pub stdout: bool,
195    /// Generate only this commit, or tag.
196    #[arg(
197        long,
198        conflicts_with_all = ["milestone", "since", "until"],
199    )]
200    pub specific: Option<String>,
201    /// Include all commits of this milestone.
202    #[arg(
203        long,
204        conflicts_with_all = ["since", "until"],
205    )]
206    pub milestone: Option<String>,
207    /// Include all commits in \"since..until\".
208    #[arg(long)]
209    pub since: Option<String>,
210    /// Include all commits in \"since..until\".
211    #[arg(long, requires = "since")]
212    pub until: Option<String>,
213}
214
215/// Generate a new release. By default, use the last tag present in the repo, sorted using the [semver](https://semver.org/) format.
216#[derive(Debug, Clone, Args)]
217pub struct Release {
218    /// Path to the changelog file.
219    #[arg(
220        short,
221        long,
222        default_value = "CHANGELOG.md",
223        value_hint = ValueHint::FilePath,
224    )]
225    pub file: Option<PathBuf>,
226    /// Version number for the release. If omitted, use the last tag present in the repo.
227    #[arg(
228        short,
229        long,
230        num_args(0..=1),
231        default_missing_value=None
232    )]
233    pub version: Option<Version>,
234    /// Previous version number. Used for the diff.
235    #[arg(long)]
236    pub previous_version: Option<Version>,
237    /// We use the Github link to produce the tags diff.
238    #[arg(long, default_value_t)]
239    pub provider: GitProvider,
240    /// Needed for the tags diff PRs. Example: 'wiiznokes/changelog-generator'. Already defined for you in Github Actions.
241    #[arg(long)]
242    pub repo: Option<String>,
243    /// Omit the commit history between releases.
244    #[arg(long)]
245    pub omit_diff: bool,
246    /// Override the release with the same version if it exist, by replacing all the existing release notes.
247    #[arg(long)]
248    pub force: bool,
249    /// Add this text as a header of the release. If a header already exist, it will be inserted before the existing one.
250    #[arg(long)]
251    pub header: Option<String>,
252    /// Merge older dev version into this new release
253    #[arg(long, default_value_t)]
254    pub merge_dev_versions: MergeDevVersions,
255    /// Print the result on the standard output.
256    #[arg(long)]
257    pub stdout: bool,
258}
259
260/// Validate a changelog syntax
261#[derive(Debug, Clone, Args)]
262pub struct Validate {
263    /// Path to the changelog file.
264    #[arg(
265        short,
266        long,
267        default_value = "CHANGELOG.md",
268        value_hint = ValueHint::FilePath,
269    )]
270    pub file: Option<PathBuf>,
271    /// Format the changelog.
272    #[arg(long, alias = "fmt")]
273    pub format: bool,
274    /// Path to the commit type to changelog section map.
275    #[arg(long, value_hint = ValueHint::FilePath)]
276    pub map: Option<PathBuf>,
277    /// Show the Abstract Syntax Tree.
278    #[arg(long)]
279    pub ast: bool,
280    /// Print the result on the standard output.
281    #[arg(long)]
282    pub stdout: bool,
283}
284
285/// Show a releases on stdout. By default, show the last release.
286#[derive(Debug, Clone, Args)]
287pub struct Show {
288    /// Path to the changelog file.
289    #[arg(
290        short,
291        long,
292        default_value = "CHANGELOG.md",
293        value_hint = ValueHint::FilePath,
294    )]
295    pub file: Option<PathBuf>,
296    /// -1 being unreleased, 0 the last release, ...
297    #[arg(
298        short,
299        default_value_t = 0,
300        conflicts_with = "version",
301        allow_hyphen_values = true
302    )]
303    pub n: i32,
304    /// Show a specific version. Also accept regex. Example: 1.0.0-*
305    #[arg(
306        short,
307        long,
308        num_args(0..=1),
309        default_missing_value=None
310    )]
311    pub version: Option<Regex>,
312}
313/// Create a new changelog file with an accepted syntax
314#[derive(Debug, Clone, Args)]
315pub struct New {
316    /// Path to the changelog file.
317    #[arg(
318        short,
319        long,
320        default_value = "CHANGELOG.md",
321        value_hint = ValueHint::FilePath,
322    )]
323    pub path: Option<PathBuf>,
324    /// Override of existing file.
325    #[arg(short, long)]
326    pub force: bool,
327}
328
329/// Remove a release
330#[derive(Debug, Clone, Args)]
331pub struct Remove {
332    /// Path to the changelog file.
333    #[arg(
334        short,
335        long,
336        default_value = "CHANGELOG.md",
337        value_hint = ValueHint::FilePath,
338    )]
339    pub file: Option<PathBuf>,
340    /// Print the result on the standard output.
341    #[arg(long)]
342    pub stdout: bool,
343
344    #[clap(flatten)]
345    pub remove_id: RemoveSelection,
346}
347
348// fixme: move this to an enum https://github.com/clap-rs/clap/issues/2621
349#[derive(Debug, Clone, Args)]
350#[group(required = true, multiple = false)]
351pub struct RemoveSelection {
352    /// -1 being unreleased, 0 the last release, ...
353    #[arg(short, conflicts_with = "version", allow_hyphen_values = true)]
354    pub n: Option<i32>,
355    /// Remove a specific version. Also accept regex. Example: 1.0.0-*
356    #[arg(short, long)]
357    pub version: Option<Regex>,
358}