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 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
106impl 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 #[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#[derive(Debug, Clone, Args)]
157pub struct Generate {
158 #[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 #[arg(long, value_hint = ValueHint::FilePath)]
170 pub map: Option<PathBuf>,
171 #[arg(long, default_value_t)]
173 pub parsing: CommitMessageParsing,
174 #[arg(long)]
176 pub exclude_unidentified: bool,
177 #[arg(long)]
179 pub exclude_not_pr: bool,
180 #[arg(long, default_value_t)]
182 pub provider: GitProvider,
183 #[arg(long)]
185 pub repo: Option<String>,
186 #[arg(long)]
188 pub omit_pr_link: bool,
189 #[arg(long)]
191 pub omit_thanks: bool,
192 #[arg(long)]
194 pub stdout: bool,
195 #[arg(
197 long,
198 conflicts_with_all = ["milestone", "since", "until"],
199 )]
200 pub specific: Option<String>,
201 #[arg(
203 long,
204 conflicts_with_all = ["since", "until"],
205 )]
206 pub milestone: Option<String>,
207 #[arg(long)]
209 pub since: Option<String>,
210 #[arg(long, requires = "since")]
212 pub until: Option<String>,
213}
214
215#[derive(Debug, Clone, Args)]
217pub struct Release {
218 #[arg(
220 short,
221 long,
222 default_value = "CHANGELOG.md",
223 value_hint = ValueHint::FilePath,
224 )]
225 pub file: Option<PathBuf>,
226 #[arg(
228 short,
229 long,
230 num_args(0..=1),
231 default_missing_value=None
232 )]
233 pub version: Option<Version>,
234 #[arg(long)]
236 pub previous_version: Option<Version>,
237 #[arg(long, default_value_t)]
239 pub provider: GitProvider,
240 #[arg(long)]
242 pub repo: Option<String>,
243 #[arg(long)]
245 pub omit_diff: bool,
246 #[arg(long)]
248 pub force: bool,
249 #[arg(long)]
251 pub header: Option<String>,
252 #[arg(long, default_value_t)]
254 pub merge_dev_versions: MergeDevVersions,
255 #[arg(long)]
257 pub stdout: bool,
258}
259
260#[derive(Debug, Clone, Args)]
262pub struct Validate {
263 #[arg(
265 short,
266 long,
267 default_value = "CHANGELOG.md",
268 value_hint = ValueHint::FilePath,
269 )]
270 pub file: Option<PathBuf>,
271 #[arg(long, alias = "fmt")]
273 pub format: bool,
274 #[arg(long, value_hint = ValueHint::FilePath)]
276 pub map: Option<PathBuf>,
277 #[arg(long)]
279 pub ast: bool,
280 #[arg(long)]
282 pub stdout: bool,
283}
284
285#[derive(Debug, Clone, Args)]
287pub struct Show {
288 #[arg(
290 short,
291 long,
292 default_value = "CHANGELOG.md",
293 value_hint = ValueHint::FilePath,
294 )]
295 pub file: Option<PathBuf>,
296 #[arg(
298 short,
299 default_value_t = 0,
300 conflicts_with = "version",
301 allow_hyphen_values = true
302 )]
303 pub n: i32,
304 #[arg(
306 short,
307 long,
308 num_args(0..=1),
309 default_missing_value=None
310 )]
311 pub version: Option<Regex>,
312}
313#[derive(Debug, Clone, Args)]
315pub struct New {
316 #[arg(
318 short,
319 long,
320 default_value = "CHANGELOG.md",
321 value_hint = ValueHint::FilePath,
322 )]
323 pub path: Option<PathBuf>,
324 #[arg(short, long)]
326 pub force: bool,
327}
328
329#[derive(Debug, Clone, Args)]
331pub struct Remove {
332 #[arg(
334 short,
335 long,
336 default_value = "CHANGELOG.md",
337 value_hint = ValueHint::FilePath,
338 )]
339 pub file: Option<PathBuf>,
340 #[arg(long)]
342 pub stdout: bool,
343
344 #[clap(flatten)]
345 pub remove_id: RemoveSelection,
346}
347
348#[derive(Debug, Clone, Args)]
350#[group(required = true, multiple = false)]
351pub struct RemoveSelection {
352 #[arg(short, conflicts_with = "version", allow_hyphen_values = true)]
354 pub n: Option<i32>,
355 #[arg(short, long)]
357 pub version: Option<Regex>,
358}