From 14d2f119bc01414d90fc17e5dad9c4db5f49863d Mon Sep 17 00:00:00 2001 From: Christopher Cyclonit Klinge Date: Sun, 17 Mar 2024 17:44:27 +0100 Subject: [PATCH] refactor(config): refactor `config` into a proper module Extracted `models_v1` from `config` for future migration sub-command to `git-cliff`. --- git-cliff-core/src/changelog.rs | 17 +- git-cliff-core/src/commit.rs | 12 +- git-cliff-core/src/config.rs | 351 ----------------------- git-cliff-core/src/config/mod.rs | 9 + git-cliff-core/src/config/models_v1.rs | 89 ++++++ git-cliff-core/src/config/models_v2.rs | 292 +++++++++++++++++++ git-cliff-core/src/config/parsing.rs | 50 ++++ git-cliff-core/src/config/test.rs | 58 ++++ git-cliff-core/src/embed.rs | 2 +- git-cliff-core/src/github.rs | 2 +- git-cliff-core/src/release.rs | 2 +- git-cliff-core/src/repo.rs | 2 +- git-cliff-core/src/template.rs | 2 +- git-cliff-core/tests/integration_test.rs | 2 +- git-cliff/src/args.rs | 2 +- git-cliff/src/lib.rs | 5 +- 16 files changed, 530 insertions(+), 367 deletions(-) delete mode 100644 git-cliff-core/src/config.rs create mode 100644 git-cliff-core/src/config/mod.rs create mode 100644 git-cliff-core/src/config/models_v1.rs create mode 100644 git-cliff-core/src/config/models_v2.rs create mode 100644 git-cliff-core/src/config/parsing.rs create mode 100644 git-cliff-core/src/config/test.rs diff --git a/git-cliff-core/src/changelog.rs b/git-cliff-core/src/changelog.rs index 1210c34fd3..8f969dde4c 100644 --- a/git-cliff-core/src/changelog.rs +++ b/git-cliff-core/src/changelog.rs @@ -1,5 +1,7 @@ use crate::commit::Commit; -use crate::config::Config; +use crate::config::models_v2::{ + Config, +}; use crate::error::Result; #[cfg(feature = "github")] use crate::github::{ @@ -307,8 +309,17 @@ impl<'a> Changelog<'a> { #[cfg(test)] mod test { use super::*; - use crate::config::{ - Bump, ChangelogConfig, CommitConfig, CommitParser, CommitSortOrder, ReleaseConfig, Remote, RemoteConfig, TagsOrderBy, TextProcessor + use crate::config::models_v2::{ + Bump, + ChangelogConfig, + CommitConfig, + CommitParser, + CommitSortOrder, + ReleaseConfig, + Remote, + RemoteConfig, + TagsOrderBy, + TextProcessor, }; use pretty_assertions::assert_eq; use regex::Regex; diff --git a/git-cliff-core/src/commit.rs b/git-cliff-core/src/commit.rs index 3f923a5515..a34b9815b2 100644 --- a/git-cliff-core/src/commit.rs +++ b/git-cliff-core/src/commit.rs @@ -1,5 +1,9 @@ -use crate::config::{ - ChangelogConfig, CommitConfig, CommitParser, LinkParser, TextProcessor +use crate::config::models_v2::{ + ChangelogConfig, + CommitConfig, + CommitParser, + LinkParser, + TextProcessor }; use crate::error::{ Error as AppError, @@ -484,10 +488,10 @@ mod test { #[test] fn conventional_footers() { - let changelog_config = crate::config::ChangelogConfig { + let changelog_config = crate::config::models_v2::ChangelogConfig { ..Default::default() }; - let commit_config = crate::config::CommitConfig { + let commit_config = crate::config::models_v2::CommitConfig { parse_conventional_commits: Some(true), ..Default::default() }; diff --git a/git-cliff-core/src/config.rs b/git-cliff-core/src/config.rs deleted file mode 100644 index 2272531092..0000000000 --- a/git-cliff-core/src/config.rs +++ /dev/null @@ -1,351 +0,0 @@ -use clap::ValueEnum; -use crate::command; -use crate::error::Result; -use regex::{ - Regex, - RegexBuilder, -}; -use secrecy::SecretString; -use serde::{ - Deserialize, - Serialize, -}; -use std::ffi::OsStr; -use std::fmt; -use std::fs; -use std::path::Path; - -/// Regex for matching the metadata in Cargo.toml -const CARGO_METADATA_REGEX: &str = - r"^\[(?:workspace|package)\.metadata\.git\-cliff\."; - -/// Regex for matching the metadata in pyproject.toml -const PYPROJECT_METADATA_REGEX: &str = r"^\[(?:tool)\.git\-cliff\."; - -/// Options for ordering git tags. -#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum TagsOrderBy { - /// Whether to sort git tags according to their creation date. - Time, - /// Whether to sort git tags according to the git topology. - Topology, -} - -/// Options for ordering commits chronologically. -#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum CommitSortOrder { - /// Whether to sort starting with the oldest element. - Oldest, - /// Whether to sort starting with the newest element. - Newest, -} - -/// Configuration values. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - /// Configuration values about changelog generation. - #[serde(default)] - pub changelog: ChangelogConfig, - /// Configuration values about releases. - #[serde(default)] - pub release: ReleaseConfig, - /// Configuration values about git commits. - #[serde(default)] - pub commit: CommitConfig, - /// Configuration values about remote. - #[serde(default)] - pub remote: RemoteConfig, - /// Configuration values about bump version. - #[serde(default)] - pub bump: Bump, -} - -/// Changelog configuration. -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct ChangelogConfig { - /// A static header for the changelog. - pub header: Option, - /// A Tera template to be rendered for each release in the changelog. - pub body_template: Option, - /// A Tera template to be rendered as the changelog's footer. - pub footer_template: Option, - /// Whether to remove leading and trailing whitespaces from all lines of the changelog's body. - pub trim_body_whitespace: Option, - /// A list of postprocessors using regex to modify the changelog. - pub postprocessors: Option>, - /// Whether to exclude changes that do not belong to any group from the changelog. - pub exclude_ungrouped_changes: Option, -} - -/// Release configuration. -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct ReleaseConfig { - /// Regex to select git tags that represent releases. - /// Example: "v[0-9].*" - #[serde(with = "serde_regex", default)] - pub tags_pattern: Option, - /// Regex to select git tags that do not represent proper releases. Takes precedence over `release.tags_pattern`. - /// Changes belonging to these releases will be included in the next non-skipped release. - /// Example: "rc" - #[serde(with = "serde_regex", default)] - pub skip_tags_pattern: Option, - /// Whether to order releases chronologically or topologically. - pub order_by: Option, -} - -/// Git commit configuration -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct CommitConfig { - /// Whether to parse commits according to the conventional commits specification. - pub parse_conventional_commits: Option, - /// Whether to exclude commits that do not match the conventional commits specification from the changelog. - pub exclude_unconventional_commits: Option, - /// Whether to split commits on newlines, treating each line as an individual commit. - pub split_by_newline: Option, - - /// A list of preprocessors to modify commit messages using regex prior to further processing. - pub message_preprocessors: Option>, - /// A list of parsers using regex for extracting data from the commit message. - pub commit_parsers: Option>, - /// Whether to prevent breaking changes from being excluded by commit parsers. - pub retain_breaking_changes: Option, - /// A list of parsers using regex for extracting external references found in commit messages, and turning them into links. The gemerated links can be used in the body template as `commit.links`. - pub link_parsers: Option>, - /// Regex to select git tags that should be excluded from the changelog. - #[serde(with = "serde_regex", default)] - pub exclude_tags_pattern: Option, - /// Whether to order commits newest to oldest or oldest to newest in their group. - pub sort_order: Option, - /// Whether to limit the total number of commits to be included in the changelog. - pub max_commit_count: Option, -} - -/// Remote configuration. -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RemoteConfig { - /// GitHub remote. - pub github: Remote, -} - -/// A single remote. -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct Remote { - /// Owner of the remote. - pub owner: String, - /// Repository name. - pub repo: String, - /// Access token. - #[serde(skip_serializing)] - pub token: Option, -} - -impl fmt::Display for Remote { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}/{}", self.owner, self.repo) - } -} - -impl PartialEq for Remote { - fn eq(&self, other: &Self) -> bool { - self.to_string() == other.to_string() - } -} - -impl Remote { - /// Constructs a new instance. - pub fn new>(owner: S, repo: S) -> Self { - Self { - owner: owner.into(), - repo: repo.into(), - token: None, - } - } - - /// Returns `true` if the remote has an owner and repo. - pub fn is_set(&self) -> bool { - !self.owner.is_empty() && !self.repo.is_empty() - } -} - -/// Bump version configuration. -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct Bump { - /// Configures automatic minor version increments for feature changes. - /// - /// When `true`, a feature will always trigger a minor version update. - /// When `false`, a feature will trigger: - /// - /// - A patch version update if the major version is 0. - /// - A minor version update otherwise. - pub features_always_bump_minor: Option, - - /// Configures 0 -> 1 major version increments for breaking changes. - /// - /// When `true`, a breaking change commit will always trigger a major - /// version update (including the transition from version 0 to 1) - /// When `false`, a breaking change commit will trigger: - /// - /// - A minor version update if the major version is 0. - /// - A major version update otherwise. - pub breaking_always_bump_major: Option, -} - -/// Parser for grouping commits. -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct CommitParser { - /// SHA1 of the commit. - pub sha: Option, - /// Regex for matching the commit message. - #[serde(with = "serde_regex", default)] - pub message: Option, - /// Regex for matching the commit body. - #[serde(with = "serde_regex", default)] - pub body: Option, - /// Group of the commit. - pub group: Option, - /// Default scope of the commit. - pub default_scope: Option, - /// Commit scope for overriding the default scope. - pub scope: Option, - /// Whether to skip this commit group. - pub skip: Option, - /// Field name of the commit to match the regex against. - pub field: Option, - /// Regex for matching the field value. - #[serde(with = "serde_regex", default)] - pub pattern: Option, -} - -/// TextProcessor, e.g. for modifying commit messages. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TextProcessor { - /// Regex for matching a text to replace. - #[serde(with = "serde_regex")] - pub pattern: Regex, - /// Replacement text. - pub replace: Option, - /// Command that will be run for replacing the commit message. - pub replace_command: Option, -} - -impl TextProcessor { - /// Replaces the text with using the given pattern or the command output. - pub fn replace( - &self, - rendered: &mut String, - command_envs: Vec<(&str, &str)>, - ) -> Result<()> { - if let Some(text) = &self.replace { - *rendered = self.pattern.replace_all(rendered, text).to_string(); - } else if let Some(command) = &self.replace_command { - if self.pattern.is_match(rendered) { - *rendered = - command::run(command, Some(rendered.to_string()), command_envs)?; - } - } - Ok(()) - } -} - -/// Parser for extracting links in commits. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LinkParser { - /// Regex for finding links in the commit message. - #[serde(with = "serde_regex")] - pub pattern: Regex, - /// The string used to generate the link URL. - pub href: String, - /// The string used to generate the link text. - pub text: Option, -} - -impl Config { - /// Parses the config file and returns the values. - pub fn parse(path: &Path) -> Result { - let config_builder = if path.file_name() == Some(OsStr::new("Cargo.toml")) || - path.file_name() == Some(OsStr::new("pyproject.toml")) - { - let contents = fs::read_to_string(path)?; - let metadata_regex = RegexBuilder::new( - if path.file_name() == Some(OsStr::new("Cargo.toml")) { - CARGO_METADATA_REGEX - } else { - PYPROJECT_METADATA_REGEX - }, - ) - .multi_line(true) - .build()?; - let contents = metadata_regex.replace_all(&contents, "["); - config::Config::builder().add_source(config::File::from_str( - &contents, - config::FileFormat::Toml, - )) - } else { - config::Config::builder().add_source(config::File::from(path)) - }; - Ok(config_builder - .add_source( - config::Environment::with_prefix("GIT_CLIFF").separator("__"), - ) - .build()? - .try_deserialize()?) - } -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - use std::env; - use std::path::PathBuf; - #[test] - fn parse_config() -> Result<()> { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("parent directory not found") - .to_path_buf() - .join("config") - .join(crate::DEFAULT_CONFIG); - - const FOOTER_VALUE: &str = "test"; - const RELEASE_TAGS_PATTERN_VALUE: &str = ".*[0-9].*"; - const RELEASE_SKIP_TAGS_PATTERN_VALUE: &str = "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+"; - - env::set_var("GIT_CLIFF__CHANGELOG__FOOTER_TEMPLATE", FOOTER_VALUE); - env::set_var("GIT_CLIFF__RELEASE__TAGS_PATTERN", RELEASE_TAGS_PATTERN_VALUE); - env::set_var("GIT_CLIFF__RELEASE__SKIP_TAGS_PATTERN", RELEASE_SKIP_TAGS_PATTERN_VALUE); - - let config = Config::parse(&path)?; - - assert_eq!(Some(String::from(FOOTER_VALUE)), config.changelog.footer_template); - assert_eq!( - Some(String::from(RELEASE_TAGS_PATTERN_VALUE)), - config - .release - .tags_pattern - .map(|tags_pattern| tags_pattern.to_string()) - ); - assert_eq!( - Some(String::from(RELEASE_SKIP_TAGS_PATTERN_VALUE)), - config - .release - .skip_tags_pattern - .map(|skip_tags_pattern| skip_tags_pattern.to_string()) - ); - Ok(()) - } - - #[test] - fn remote_config() { - let remote1 = Remote::new("abc", "xyz1"); - let remote2 = Remote::new("abc", "xyz2"); - assert!(!remote1.eq(&remote2)); - assert_eq!("abc/xyz1", remote1.to_string()); - assert!(remote1.is_set()); - assert!(!Remote::new("", "test").is_set()); - assert!(!Remote::new("test", "").is_set()); - assert!(!Remote::new("", "").is_set()); - } -} diff --git a/git-cliff-core/src/config/mod.rs b/git-cliff-core/src/config/mod.rs new file mode 100644 index 0000000000..623c11366d --- /dev/null +++ b/git-cliff-core/src/config/mod.rs @@ -0,0 +1,9 @@ +/// Deprecated Config models for git-cliff. +pub mod models_v1; +/// Current Config models for git-cliff. +pub mod models_v2; +/// Parsing for git-cliff Config. +pub mod parsing; +/// Tests for git-cliff Config. +#[cfg(test)] +pub mod test; diff --git a/git-cliff-core/src/config/models_v1.rs b/git-cliff-core/src/config/models_v1.rs new file mode 100644 index 0000000000..b38b51aaec --- /dev/null +++ b/git-cliff-core/src/config/models_v1.rs @@ -0,0 +1,89 @@ + +use super::models_v2::{ + Bump, + CommitParser, + LinkParser, + RemoteConfig, + TextProcessor, +}; +use regex::Regex; +use serde::{ + Deserialize, + Serialize, +}; + +/// Configuration values. +#[deprecated(since="3.0.0", note="deprecated in favor of models_v2")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + /// Configuration values about changelog generation. + #[allow(deprecated)] + #[serde(default)] + pub changelog: ChangelogConfig, + /// Configuration values about git. + #[allow(deprecated)] + #[serde(default)] + pub git: GitConfig, + /// Configuration values about remote. + #[serde(default)] + pub remote: RemoteConfig, + /// Configuration values about bump version. + #[serde(default)] + pub bump: Bump, +} + +/// Changelog configuration. +#[deprecated(since="3.0.0", note="deprecated in favor of models_v2")] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct ChangelogConfig { + /// Changelog header. + pub header: Option, + /// Changelog body, template. + pub body: Option, + /// Changelog footer. + pub footer: Option, + /// Trim the template. + pub trim: Option, + /// Changelog postprocessors. + pub postprocessors: Option>, +} + +/// Git configuration +#[deprecated(since="3.0.0", note="deprecated in favor of models_v2")] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct GitConfig { + /// Whether to enable parsing conventional commits. + pub conventional_commits: Option, + /// Whether to filter out unconventional commits. + pub filter_unconventional: Option, + /// Whether to split commits by line, processing each line as an individual + /// commit. + pub split_commits: Option, + + /// Git commit preprocessors. + pub commit_preprocessors: Option>, + /// Git commit parsers. + pub commit_parsers: Option>, + /// Whether to protect all breaking changes from being skipped by a commit + /// parser. + pub protect_breaking_commits: Option, + /// Link parsers. + pub link_parsers: Option>, + /// Whether to filter out commits. + pub filter_commits: Option, + /// Blob pattern for git tags. + #[serde(with = "serde_regex", default)] + pub tag_pattern: Option, + /// Regex to skip matched tags. + #[serde(with = "serde_regex", default)] + pub skip_tags: Option, + /// Regex to ignore matched tags. + #[serde(with = "serde_regex", default)] + pub ignore_tags: Option, + /// Whether to sort tags topologically. + pub topo_order: Option, + /// Sorting of the commits inside sections. + pub sort_commits: Option, + /// Limit the number of commits included in the changelog. + pub limit_commits: Option, +} diff --git a/git-cliff-core/src/config/models_v2.rs b/git-cliff-core/src/config/models_v2.rs new file mode 100644 index 0000000000..0a652c96c5 --- /dev/null +++ b/git-cliff-core/src/config/models_v2.rs @@ -0,0 +1,292 @@ + +use clap::ValueEnum; +use crate::command; +use crate::error::Result; +use regex::Regex; +use secrecy::SecretString; +use serde::{ + Deserialize, + Serialize, +}; +use std::fmt; + +/// Options for ordering git tags. +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum TagsOrderBy { + /// Whether to sort git tags according to their creation date. + Time, + /// Whether to sort git tags according to the git topology. + Topology, +} + +/// Options for ordering commits chronologically. +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum CommitSortOrder { + /// Whether to sort starting with the oldest element. + Oldest, + /// Whether to sort starting with the newest element. + Newest, +} + +/// Configuration values. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + /// Configuration values about changelog generation. + #[serde(default)] + pub changelog: ChangelogConfig, + /// Configuration values about releases. + #[serde(default)] + pub release: ReleaseConfig, + /// Configuration values about git commits. + #[serde(default)] + pub commit: CommitConfig, + /// Configuration values about remote. + #[serde(default)] + pub remote: RemoteConfig, + /// Configuration values about bump version. + #[serde(default)] + pub bump: Bump, +} + +/// Changelog configuration. +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct ChangelogConfig { + /// A static header for the changelog. + pub header: Option, + /// A Tera template to be rendered for each release in the changelog. + pub body_template: Option, + /// A Tera template to be rendered as the changelog's footer. + pub footer_template: Option, + /// Whether to remove leading and trailing whitespaces from all lines of the changelog's body. + pub trim_body_whitespace: Option, + /// A list of postprocessors using regex to modify the changelog. + pub postprocessors: Option>, + /// Whether to exclude changes that do not belong to any group from the changelog. + pub exclude_ungrouped_changes: Option, +} + +/// Release configuration. +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct ReleaseConfig { + /// Regex to select git tags that represent releases. + /// Example: "v[0-9].*" + #[serde(with = "serde_regex", default)] + pub tags_pattern: Option, + /// Regex to select git tags that do not represent proper releases. Takes precedence over `release.tags_pattern`. + /// Changes belonging to these releases will be included in the next non-skipped release. + /// Example: "rc" + #[serde(with = "serde_regex", default)] + pub skip_tags_pattern: Option, + /// Whether to order releases chronologically or topologically. + pub order_by: Option, +} + +/// Git commit configuration +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct CommitConfig { + /// Whether to parse commits according to the conventional commits specification. + pub parse_conventional_commits: Option, + /// Whether to exclude commits that do not match the conventional commits specification from the changelog. + pub exclude_unconventional_commits: Option, + /// Whether to split commits on newlines, treating each line as an individual commit. + pub split_by_newline: Option, + + /// A list of preprocessors to modify commit messages using regex prior to further processing. + pub message_preprocessors: Option>, + /// A list of parsers using regex for extracting data from the commit message. + pub commit_parsers: Option>, + /// Whether to prevent breaking changes from being excluded by commit parsers. + pub retain_breaking_changes: Option, + /// A list of parsers using regex for extracting external references found in commit messages, and turning them into links. The gemerated links can be used in the body template as `commit.links`. + pub link_parsers: Option>, + /// Regex to select git tags that should be excluded from the changelog. + #[serde(with = "serde_regex", default)] + pub exclude_tags_pattern: Option, + /// Whether to order commits newest to oldest or oldest to newest in their group. + pub sort_order: Option, + /// Whether to limit the total number of commits to be included in the changelog. + pub max_commit_count: Option, +} + +/// Remote configuration. +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct RemoteConfig { + /// GitHub remote. + pub github: Remote, +} + +/// A single remote. +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct Remote { + /// Owner of the remote. + pub owner: String, + /// Repository name. + pub repo: String, + /// Access token. + #[serde(skip_serializing)] + pub token: Option, +} + +impl fmt::Display for Remote { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}/{}", self.owner, self.repo) + } +} + +impl PartialEq for Remote { + fn eq(&self, other: &Self) -> bool { + self.to_string() == other.to_string() + } +} + +impl Remote { + /// Constructs a new instance. + pub fn new>(owner: S, repo: S) -> Self { + Self { + owner: owner.into(), + repo: repo.into(), + token: None, + } + } + + /// Returns `true` if the remote has an owner and repo. + pub fn is_set(&self) -> bool { + !self.owner.is_empty() && !self.repo.is_empty() + } +} + +/// Bump version configuration. +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct Bump { + /// Configures automatic minor version increments for feature changes. + /// + /// When `true`, a feature will always trigger a minor version update. + /// When `false`, a feature will trigger: + /// + /// - A patch version update if the major version is 0. + /// - A minor version update otherwise. + pub features_always_bump_minor: Option, + + /// Configures 0 -> 1 major version increments for breaking changes. + /// + /// When `true`, a breaking change commit will always trigger a major + /// version update (including the transition from version 0 to 1) + /// When `false`, a breaking change commit will trigger: + /// + /// - A minor version update if the major version is 0. + /// - A major version update otherwise. + pub breaking_always_bump_major: Option, +} + +/// Parser for grouping commits. +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct CommitParser { + /// SHA1 of the commit. + pub sha: Option, + /// Regex for matching the commit message. + #[serde(with = "serde_regex", default)] + pub message: Option, + /// Regex for matching the commit body. + #[serde(with = "serde_regex", default)] + pub body: Option, + /// Group of the commit. + pub group: Option, + /// Default scope of the commit. + pub default_scope: Option, + /// Commit scope for overriding the default scope. + pub scope: Option, + /// Whether to skip this commit group. + pub skip: Option, + /// Field name of the commit to match the regex against. + pub field: Option, + /// Regex for matching the field value. + #[serde(with = "serde_regex", default)] + pub pattern: Option, +} + +/// TextProcessor, e.g. for modifying commit messages. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TextProcessor { + /// Regex for matching a text to replace. + #[serde(with = "serde_regex")] + pub pattern: Regex, + /// Replacement text. + pub replace: Option, + /// Command that will be run for replacing the commit message. + pub replace_command: Option, +} + +impl TextProcessor { + /// Replaces the text with using the given pattern or the command output. + pub fn replace( + &self, + rendered: &mut String, + command_envs: Vec<(&str, &str)>, + ) -> Result<()> { + if let Some(text) = &self.replace { + *rendered = self.pattern.replace_all(rendered, text).to_string(); + } else if let Some(command) = &self.replace_command { + if self.pattern.is_match(rendered) { + *rendered = + command::run(command, Some(rendered.to_string()), command_envs)?; + } + } + Ok(()) + } +} + +/// Parser for extracting links in commits. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LinkParser { + /// Regex for finding links in the commit message. + #[serde(with = "serde_regex")] + pub pattern: Regex, + /// The string used to generate the link URL. + pub href: String, + /// The string used to generate the link text. + pub text: Option, +} + +impl Config { + + /// Creates a v2 Config from a deprecated v1 Config. + #[allow(deprecated)] + pub fn from(config_v1: super::models_v1::Config) -> Config { + Config{ + changelog: ChangelogConfig { + header: config_v1.changelog.header, + body_template: config_v1.changelog.body, + footer_template: config_v1.changelog.footer, + trim_body_whitespace: config_v1.changelog.trim, + postprocessors: config_v1.changelog.postprocessors, + exclude_ungrouped_changes: config_v1.git.filter_commits, + }, + release: ReleaseConfig { + tags_pattern: config_v1.git.tag_pattern, + skip_tags_pattern: config_v1.git.ignore_tags, + order_by: Some(if config_v1.git.topo_order.unwrap() { + TagsOrderBy::Topology + } + else { + TagsOrderBy::Time + }), + }, + commit: CommitConfig { + sort_order: config_v1.git.sort_commits.map(|s| CommitSortOrder::from_str(&s, true).expect("Incorrect config value for 'sort_commits'")), + max_commit_count: config_v1.git.limit_commits, + split_by_newline: config_v1.git.split_commits, + exclude_tags_pattern: config_v1.git.skip_tags, + message_preprocessors: config_v1.git.commit_preprocessors, + link_parsers: config_v1.git.link_parsers, + parse_conventional_commits: config_v1.git.conventional_commits, + exclude_unconventional_commits: config_v1.git.filter_unconventional, + commit_parsers: config_v1.git.commit_parsers, + retain_breaking_changes: config_v1.git.protect_breaking_commits, + }, + remote: config_v1.remote.clone(), + bump: config_v1.bump.clone() + } + } +} diff --git a/git-cliff-core/src/config/parsing.rs b/git-cliff-core/src/config/parsing.rs new file mode 100644 index 0000000000..0eb8dce7f8 --- /dev/null +++ b/git-cliff-core/src/config/parsing.rs @@ -0,0 +1,50 @@ + +use crate::error::Result; +use regex::RegexBuilder; +use serde::Deserialize; +use std::{ + ffi::OsStr, + fs, + path::Path, +}; + +/// Regex for matching the metadata in Cargo.toml +const CARGO_METADATA_REGEX: &str = r"^\[(?:workspace|package)\.metadata\.git\-cliff\."; + +/// Regex for matching the metadata in pyproject.toml +const PYPROJECT_METADATA_REGEX: &str = r"^\[(?:tool)\.git\-cliff\."; + +/// Loads configuration from a file and GIT_CLIFF_* environment variables. +pub fn parse<'de, T: Deserialize<'de>>(path: &Path) -> Result { + + let file_name = path.file_name(); + let config_builder = if file_name == Some(OsStr::new("Cargo.toml")) || file_name == Some(OsStr::new("pyproject.toml")) + { + let contents = fs::read_to_string(path)?; + let metadata_regex = RegexBuilder::new( + if path.file_name() == Some(OsStr::new("Cargo.toml")) { + CARGO_METADATA_REGEX + } else { + PYPROJECT_METADATA_REGEX + }, + ) + .multi_line(true) + .build()?; + let contents = metadata_regex.replace_all(&contents, "["); + config::Config::builder().add_source(config::File::from_str( + &contents, + config::FileFormat::Toml, + )) + } + else { + config::Config::builder().add_source(config::File::from(path)) + }; + + Ok(config_builder + .add_source( + config::Environment::with_prefix("GIT_CLIFF").separator("__") + ) + .build()? + .try_deserialize()? + ) +} diff --git a/git-cliff-core/src/config/test.rs b/git-cliff-core/src/config/test.rs new file mode 100644 index 0000000000..5f0efbcfc0 --- /dev/null +++ b/git-cliff-core/src/config/test.rs @@ -0,0 +1,58 @@ + +use crate::config::parsing; +use crate::error::Result; +use super::models_v2::{ + Config, + Remote +}; +use std::env; +use std::path::PathBuf; + +#[test] +fn parse_config() -> Result<()> { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("parent directory not found") + .to_path_buf() + .join("config") + .join(crate::DEFAULT_CONFIG); + + const FOOTER_VALUE: &str = "test"; + const RELEASE_TAGS_PATTERN_VALUE: &str = ".*[0-9].*"; + const RELEASE_SKIP_TAGS_PATTERN_VALUE: &str = "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+"; + + env::set_var("GIT_CLIFF__CHANGELOG__FOOTER_TEMPLATE", FOOTER_VALUE); + env::set_var("GIT_CLIFF__RELEASE__TAGS_PATTERN", RELEASE_TAGS_PATTERN_VALUE); + env::set_var("GIT_CLIFF__RELEASE__SKIP_TAGS_PATTERN", RELEASE_SKIP_TAGS_PATTERN_VALUE); + + let config = parsing::parse::(&path)?; + + assert_eq!(Some(String::from(FOOTER_VALUE)), config.changelog.footer_template); + assert_eq!( + Some(String::from(RELEASE_TAGS_PATTERN_VALUE)), + config + .release + .tags_pattern + .map(|tags_pattern| tags_pattern.to_string()) + ); + assert_eq!( + Some(String::from(RELEASE_SKIP_TAGS_PATTERN_VALUE)), + config + .release + .skip_tags_pattern + .map(|skip_tags_pattern| skip_tags_pattern.to_string()) + ); + Ok(()) +} + +#[test] +fn remote_config() { + let remote1 = Remote::new("abc", "xyz1"); + let remote2 = Remote::new("abc", "xyz2"); + assert!(!remote1.eq(&remote2)); + assert_eq!("abc/xyz1", remote1.to_string()); + assert!(remote1.is_set()); + assert!(!Remote::new("", "test").is_set()); + assert!(!Remote::new("test", "").is_set()); + assert!(!Remote::new("", "").is_set()); +} \ No newline at end of file diff --git a/git-cliff-core/src/embed.rs b/git-cliff-core/src/embed.rs index c4b18275cb..9707866562 100644 --- a/git-cliff-core/src/embed.rs +++ b/git-cliff-core/src/embed.rs @@ -1,4 +1,4 @@ -use crate::config::Config; +use crate::config::models_v2::Config; use crate::error::{ Error, Result, diff --git a/git-cliff-core/src/github.rs b/git-cliff-core/src/github.rs index 4548814604..73e9ad162e 100644 --- a/git-cliff-core/src/github.rs +++ b/git-cliff-core/src/github.rs @@ -1,4 +1,4 @@ -use crate::config::Remote; +use crate::config::models_v2::Remote; use crate::error::*; use futures::{ future, diff --git a/git-cliff-core/src/release.rs b/git-cliff-core/src/release.rs index 34a5c40557..d9b25606dc 100644 --- a/git-cliff-core/src/release.rs +++ b/git-cliff-core/src/release.rs @@ -1,5 +1,5 @@ use crate::commit::Commit; -use crate::config::Bump; +use crate::config::models_v2::Bump; use crate::error::Result; #[cfg(feature = "github")] use crate::github::{ diff --git a/git-cliff-core/src/repo.rs b/git-cliff-core/src/repo.rs index d4c5fe8e84..d52b106677 100644 --- a/git-cliff-core/src/repo.rs +++ b/git-cliff-core/src/repo.rs @@ -1,4 +1,4 @@ -use crate::config::{ +use crate::config::models_v2::{ Remote, TagsOrderBy, }; diff --git a/git-cliff-core/src/template.rs b/git-cliff-core/src/template.rs index d4542c8fa4..6db4a67fd0 100644 --- a/git-cliff-core/src/template.rs +++ b/git-cliff-core/src/template.rs @@ -1,5 +1,5 @@ use crate::{ - config::TextProcessor, + config::models_v2::TextProcessor, error::{ Error, Result, diff --git a/git-cliff-core/tests/integration_test.rs b/git-cliff-core/tests/integration_test.rs index 462d59f08e..4dcf3ff2d4 100644 --- a/git-cliff-core/tests/integration_test.rs +++ b/git-cliff-core/tests/integration_test.rs @@ -2,7 +2,7 @@ use git_cliff_core::commit::{ Commit, Signature, }; -use git_cliff_core::config::{ +use git_cliff_core::config::models_v2::{ ChangelogConfig, CommitParser, CommitConfig, diff --git a/git-cliff/src/args.rs b/git-cliff/src/args.rs index d8f18d90ce..6852b5a2ec 100644 --- a/git-cliff/src/args.rs +++ b/git-cliff/src/args.rs @@ -13,7 +13,7 @@ use clap::{ ValueEnum, }; use git_cliff_core::{ - config::{ + config::models_v2::{ CommitSortOrder, Remote, TagsOrderBy diff --git a/git-cliff/src/lib.rs b/git-cliff/src/lib.rs index 9bf401a947..f57a63de09 100644 --- a/git-cliff/src/lib.rs +++ b/git-cliff/src/lib.rs @@ -19,12 +19,13 @@ use args::{ }; use git_cliff_core::changelog::Changelog; use git_cliff_core::commit::Commit; -use git_cliff_core::config::{ +use git_cliff_core::config::models_v2::{ CommitParser, CommitSortOrder, Config, TagsOrderBy, }; +use git_cliff_core::config::parsing; use git_cliff_core::embed::{ BuiltinConfig, EmbeddedConfig, @@ -346,7 +347,7 @@ pub fn run(mut args: Opt) -> Result<()> { info!("Using built-in configuration file: {name}"); config } else if path.exists() { - Config::parse(&path)? + parsing::parse(&path)? } else { if !args.context { warn!(