use std::fs::File; use std::io::Read; use std::path::{Path, PathBuf}; use std::{collections::HashSet, fmt::Display}; use changelog::fmt::SortOptions; use clap::{arg, Args, Parser, Subcommand, ValueHint}; use changelog::ser::{Options, OptionsRelease}; use clap::ValueEnum; use indexmap::IndexMap; use regex::Regex; use semver::Version; use serde::{Deserialize, Serialize}; use crate::git_provider::GitProvider; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MapMessageToSection(pub IndexMap>); impl Default for MapMessageToSection { fn default() -> Self { let map = include_str!("../res/map_commit_type_to_section.json"); serde_json::de::from_str(map).unwrap() } } impl MapMessageToSection { pub fn to_fmt_options(self) -> changelog::fmt::Options { changelog::fmt::Options { sort_options: SortOptions { section_order: self.0.into_iter().map(|(section, _)| section).collect(), ..Default::default() }, } } pub fn into_changelog_ser_options(self) -> Options { Options { release_option: OptionsRelease { ..Default::default() }, } } pub fn map_section(&self, section: &str) -> Option { let section_normalized = section.to_lowercase(); for (section, needles) in &self.0 { for needle in needles { let needle_normalized = needle.to_lowercase(); if section_normalized == needle_normalized { return Some(section.to_owned()); } } } None } /// Best effort recognition pub fn try_find_section(&self, (message, desc): (&str, &str)) -> Option { let message_normalized = message.to_lowercase(); let desc_normalized = desc.to_lowercase(); for (section, needles) in &self.0 { for needle in needles { let needle_normalized = needle.to_lowercase(); if message_normalized.contains(&needle_normalized) { return Some(section.to_owned()); } if desc_normalized.contains(&needle_normalized) { return Some(section.to_owned()); } } } None } pub fn try_new>(path: Option

) -> anyhow::Result { match path { Some(path) => { let mut file = File::open(&path)?; let mut content = Vec::new(); file.read_to_end(&mut content)?; let map = serde_json::de::from_slice(&content)?; Ok(map) } None => Ok(MapMessageToSection::default()), } } } #[derive(ValueEnum, Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub enum CommitMessageParsing { #[default] Smart, Strict, } // todo: use derive_more::Display when this issue is resolved // https://github.com/JelteF/derive_more/issues/216 impl Display for CommitMessageParsing { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CommitMessageParsing::Smart => write!(f, "smart"), CommitMessageParsing::Strict => write!(f, "strict"), } } } #[derive(ValueEnum, Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub enum MergeDevVersions { /// Yes if the version is stable, no otherwise #[default] Auto, No, Yes, } impl Display for MergeDevVersions { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MergeDevVersions::Auto => write!(f, "auto"), MergeDevVersions::No => write!(f, "no"), MergeDevVersions::Yes => write!(f, "yes"), } } } #[derive(Debug, Clone, Parser)] #[command(name = "changelog", version, about = "Changelog generator", long_about = None)] pub struct Cli { #[command(subcommand)] pub command: Commands, } #[derive(Debug, Clone, Subcommand)] pub enum Commands { #[command(alias = "gen")] Generate(Generate), Release(Release), Validate(Validate), Show(Show), New(New), #[command(aliases = ["delete", "rm"])] Remove(Remove), } /// Generate release notes. By default, generate from the last release in the changelog to HEAD. #[derive(Debug, Clone, Args)] pub struct Generate { /// Path to the changelog file. #[arg( short, long, default_value = "CHANGELOG.md", value_hint = ValueHint::FilePath, short_alias = 'o', alias = "output", )] pub file: Option, /// Path to the commit type to changelog section map. #[arg(long, value_hint = ValueHint::FilePath)] pub map: Option, /// Parsing of the commit message. #[arg(long, default_value_t)] pub parsing: CommitMessageParsing, /// Don't include unidentified commits. #[arg(long)] pub exclude_unidentified: bool, /// Don't include commits which are not attached to a pull request. #[arg(long)] pub exclude_not_pr: bool, /// We use the Github api to map commit sha to PRs. #[arg(long, default_value_t)] pub provider: GitProvider, /// Needed for fetching PRs. Example: 'wiiznokes/changelog-generator'. Already defined for you in Github Actions. #[arg(long)] pub repo: Option, /// Omit the PR link from the output. #[arg(long)] pub omit_pr_link: bool, /// Omit contributors' acknowledgements/mention. #[arg(long)] pub omit_thanks: bool, /// Print the result on the standard output. #[arg(long)] pub stdout: bool, /// Generate only this commit, or tag. #[arg( long, conflicts_with_all = ["milestone", "since", "until"], )] pub specific: Option, /// Include all commits of this milestone. #[arg( long, conflicts_with_all = ["since", "until"], )] pub milestone: Option, /// Include all commits in \"since..until\". #[arg(long)] pub since: Option, /// Include all commits in \"since..until\". #[arg(long, requires = "since")] pub until: Option, } /// Generate a new release. By default, use the last tag present in the repo, sorted using the [semver](https://semver.org/) format. #[derive(Debug, Clone, Args)] pub struct Release { /// Path to the changelog file. #[arg( short, long, default_value = "CHANGELOG.md", value_hint = ValueHint::FilePath, )] pub file: Option, /// Version number for the release. If omitted, use the last tag present in the repo. #[arg( short, long, num_args(0..=1), default_missing_value=None )] pub version: Option, /// Previous version number. Used for the diff. #[arg(long)] pub previous_version: Option, /// We use the Github link to produce the tags diff. #[arg(long, default_value_t)] pub provider: GitProvider, /// Needed for the tags diff PRs. Example: 'wiiznokes/changelog-generator'. Already defined for you in Github Actions. #[arg(long)] pub repo: Option, /// Omit the commit history between releases. #[arg(long)] pub omit_diff: bool, /// Override the release with the same version if it exist, by replacing all the existing release notes. #[arg(long)] pub force: bool, /// Add this text as a header of the release. If a header already exist, it will be inserted before the existing one. #[arg(long)] pub header: Option, /// Merge older dev version into this new release #[arg(long, default_value_t)] pub merge_dev_versions: MergeDevVersions, /// Print the result on the standard output. #[arg(long)] pub stdout: bool, } /// Validate a changelog syntax #[derive(Debug, Clone, Args)] pub struct Validate { /// Path to the changelog file. #[arg( short, long, default_value = "CHANGELOG.md", value_hint = ValueHint::FilePath, )] pub file: Option, /// Format the changelog. #[arg(long, alias = "fmt")] pub format: bool, /// Path to the commit type to changelog section map. #[arg(long, value_hint = ValueHint::FilePath)] pub map: Option, /// Show the Abstract Syntax Tree. #[arg(long)] pub ast: bool, /// Print the result on the standard output. #[arg(long)] pub stdout: bool, } /// Show a releases on stdout. By default, show the last release. #[derive(Debug, Clone, Args)] pub struct Show { /// Path to the changelog file. #[arg( short, long, default_value = "CHANGELOG.md", value_hint = ValueHint::FilePath, )] pub file: Option, /// -1 being unreleased, 0 the last release, ... #[arg( short, default_value_t = 0, conflicts_with = "version", allow_hyphen_values = true )] pub n: i32, /// Show a specific version. Also accept regex. Example: 1.0.0-* #[arg( short, long, num_args(0..=1), default_missing_value=None )] pub version: Option, } /// Create a new changelog file with an accepted syntax #[derive(Debug, Clone, Args)] pub struct New { /// Path to the changelog file. #[arg( short, long, default_value = "CHANGELOG.md", value_hint = ValueHint::FilePath, )] pub path: Option, /// Override of existing file. #[arg(short, long)] pub force: bool, } /// Remove a release #[derive(Debug, Clone, Args)] pub struct Remove { /// Path to the changelog file. #[arg( short, long, default_value = "CHANGELOG.md", value_hint = ValueHint::FilePath, )] pub file: Option, /// Print the result on the standard output. #[arg(long)] pub stdout: bool, #[clap(flatten)] pub remove_id: RemoveSelection, } // fixme: move this to an enum https://github.com/clap-rs/clap/issues/2621 #[derive(Debug, Clone, Args)] #[group(required = true, multiple = false)] pub struct RemoveSelection { /// -1 being unreleased, 0 the last release, ... #[arg(short, conflicts_with = "version", allow_hyphen_values = true)] pub n: Option, /// Remove a specific version. Also accept regex. Example: 1.0.0-* #[arg(short, long)] pub version: Option, }