From d0af91c8e1d3c6ed74b1dda5c8aa34e2c37ddc43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 15 Sep 2025 15:05:17 +0200 Subject: [PATCH] change default config file to include comments --- src/action_init.rs | 2 +- src/assets/config_file_template.toml | 78 ++++++++++++++++++++++++++++ src/config.rs | 36 +++++++------ src/git.rs | 5 +- src/main.rs | 4 +- src/store.rs | 16 +++--- src/utils/empty_to_none.rs | 37 +++++++++++++ src/utils/mod.rs | 1 + 8 files changed, 151 insertions(+), 28 deletions(-) create mode 100644 src/assets/config_file_template.toml create mode 100644 src/utils/empty_to_none.rs create mode 100644 src/utils/mod.rs diff --git a/src/action_init.rs b/src/action_init.rs index 9a0b7df..831a4b2 100644 --- a/src/action_init.rs +++ b/src/action_init.rs @@ -29,7 +29,7 @@ pub fn cl_init(opts: ClInit) -> anyhow::Result<()> { "Creating clpack config file: {}", opts.config_path.display() ); - file.write_all(toml::to_string_pretty(&default_config)?.as_bytes())?; + file.write_all(crate::config::CONFIG_FILE_TEMPLATE.as_bytes())?; } else { println!( "Loading existing config file: {}", diff --git a/src/assets/config_file_template.toml b/src/assets/config_file_template.toml new file mode 100644 index 0000000..78faa89 --- /dev/null +++ b/src/assets/config_file_template.toml @@ -0,0 +1,78 @@ +# Configuration for clpack - changelog keeping utility +# https://github.com/MightyPork/clpack +# +# To add a changelog entry manually, place it in a .md file in changelog/entries/ + +# Folder for data files - clpack will manage contents of this folder. +data_folder = "changelog" + +# ID of the default channel - this only matters inside this config file +default_channel = "default" + +# Path or file name of the default changelog file, relative to the root of the project. +# +# The name is used as-is. +changelog_file_default = "CHANGELOG.md" + +# Path or file of a channel-specific changelog file, relative to the root of the project. +# +# Placeholders supported are: +# - `{channel}`, `{Channel}`, `{CHANNEL}` - Channel ID in the respective capitalization +changelog_file_channel = "CHANGELOG-{CHANNEL}.md" + +# Title of the changelog file, stripped and put back in front when packing changelog entries +changelog_header = ''' +# Changelog + +''' + +# Pattern for release header +release_header = "[{VERSION}] - {DATE}" + +# Date format (strftime-based) +# +# For supported patterns, see https://docs.rs/chrono/latest/chrono/format/strftime/index.html +date_format = "%Y-%m-%d" + +# Changelog sections suggested when creating a new entry. +# +# Users may also specify custom section names when writing the changelog file. +# +# Changelog entries under each section will be grouped in the packed changelog. +sections = [ + "Fixes", + "Improvements", + "New features", + "Internal", +] + +# Regex pattern to extract issue number from a branch name. +# There should be one capture group that is the number. +# +# If empty, no branch identification will be attempted. +# +# The default pattern matches 1234-gitlab-style and SW-1234-youtrack-style +branch_issue_pattern = '/^((?:SW-)?\d+)-.*/' + +# Regex pattern to extract release number from a branch name. +# There should be exactly one capture group that is the version. +# +# If empty, no branch identification will be attempted. +# +# The default pattern matches e.g. rel/1.2 +branch_version_pattern = '/^rel\/([\d.]+)$/' + +# Changelog channels & how to identify them from git branch names. +# To add a new release channel, just add it here. +# At least one channel must be defined - see the config option `default_channel` +# +# Format: key=value +# +# - key - changelog ID; this will be used in the channel file name. Examples: default, eap, beta +# - value - git branch name to recognize the channel. This is a regex pattern. +# +# For simple branch names, e.g. `main`, `master`, `test`, write the name simply as string. +# +# To specify a regex pattern (wildcard name), enclose it in slashes, e.g. '/^release\//' +[channels] +default = '/^(?:main|master)$/' diff --git a/src/config.rs b/src/config.rs index 3db6e50..5ff023b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,30 +11,35 @@ pub type VersionName = String; /// e.g. SW-1234-stuff-is-broken (without .md) pub type EntryName = String; +pub const CONFIG_FILE_TEMPLATE: &str = include_str!("assets/config_file_template.toml"); + +#[cfg(test)] +#[test] +fn test_template_file() { + // Check 1. that the example config is valid, and 2. that it matches the defaults in the struct + let parsed: Config = toml::from_str(CONFIG_FILE_TEMPLATE).unwrap(); + let def = Config::default(); + assert_eq!(parsed, def); +} + /// Main app configuration file -#[derive(Debug, Serialize, Deserialize, SmartDefault)] +#[derive(Debug, Serialize, Deserialize, SmartDefault, PartialEq, Clone)] #[serde(deny_unknown_fields, default)] pub struct Config { - /// Folder for data files - the tool will manage contents of this folder. - /// Changelog entries are simple text files that may be edited manually - /// if corrections need to be made. + /// Name / path of the folder managed by clpack #[default = "changelog"] pub data_folder: String, - /// ID of the default channel - this only matters inside this config file + /// ID of the default channel #[default = "default"] pub default_channel: String, - /// Path or file name of the default changelog file, relative to the root of the project. - /// - /// The name is used as-is. + /// Path or file name of the default changelog file, relative to project root (CWD) #[default = "CHANGELOG.md"] pub changelog_file_default: String, - /// Path or file of a channel-specific changelog file, relative to the root of the project. - /// - /// Placeholders supported are: - /// - `{channel}`, `{Channel}`, `{CHANNEL}` - Channel ID in the respective capitalization + /// Path or file of a channel-specific changelog file, relative to project root (CWD). + /// Supports placeholder `{channel}`, `{Channel}`, `{CHANNEL}` #[default = "CHANGELOG-{CHANNEL}.md"] pub changelog_file_channel: String, @@ -51,10 +56,9 @@ pub struct Config { pub date_format: String, /// Changelog sections suggested when creating a new entry. + /// The order is maintained. /// - /// Users may also specify a custom section name. - /// - /// Changelog entries under each section will be grouped in the packed changelog. + /// Users may also specify custom section names when writing the changelog file. #[default(vec![ "Fixes".to_string(), "Improvements".to_string(), @@ -97,6 +101,6 @@ pub struct Config { /// If None, no branch identification will be attempted. /// /// TODO attempt to parse version from package.json, composer.json, Cargo.toml and others - #[default(Some(r"/^rel\/(\d+\.\d+)$/".to_string()))] + #[default(Some(r"/^rel\/([\d.]+)$/".to_string()))] pub branch_version_pattern: Option, } diff --git a/src/git.rs b/src/git.rs index 1882acb..b6ba08c 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,4 +1,5 @@ use crate::AppContext; +use crate::utils::empty_to_none::EmptyToNone; use anyhow::bail; use std::fmt::Display; use std::fmt::Formatter; @@ -79,7 +80,7 @@ impl BranchName { /// /// Aborts if the configured regex pattern is invalid. pub fn parse_version(&self, ctx: &AppContext) -> anyhow::Result> { - let Some(pat) = ctx.config.branch_version_pattern.as_ref() else { + let Some(pat) = ctx.config.branch_version_pattern.as_ref().empty_to_none() else { return Ok(None); }; self.parse_using_regex(pat, "branch_version_pattern") @@ -89,7 +90,7 @@ impl BranchName { /// /// Aborts if the configured regex pattern is invalid. pub fn parse_issue(&self, ctx: &AppContext) -> anyhow::Result> { - let Some(pat) = ctx.config.branch_issue_pattern.as_ref() else { + let Some(pat) = ctx.config.branch_issue_pattern.as_ref().empty_to_none() else { return Ok(None); }; self.parse_using_regex(pat, "branch_issue_pattern") diff --git a/src/main.rs b/src/main.rs index ea4ddde..49a0caf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use crate::action_init::{ClInit, cl_init}; +use crate::action_init::{cl_init, ClInit}; use crate::action_log::cl_log; use crate::action_pack::cl_pack; use crate::config::{ChannelName, Config}; @@ -19,6 +19,8 @@ mod action_init; mod store; +mod utils; + #[derive(Debug)] pub struct AppContext { /// Name of the cl binary diff --git a/src/store.rs b/src/store.rs index abef6df..8968fe0 100644 --- a/src/store.rs +++ b/src/store.rs @@ -243,7 +243,7 @@ impl Release { pub fn render(&self, entries_dir: impl AsRef, config: &Config) -> anyhow::Result { let mut entries_per_section = IndexMap::::new(); let entries_dir = entries_dir.as_ref(); - let unnamed = "".to_string(); + let unnamed_section = "".to_string(); for entry in &self.entries { let entry_file = entries_dir.join(&format!("{entry}.md")); @@ -258,17 +258,17 @@ impl Release { let file = OpenOptions::new().read(true).open(&entry_file)?; let reader = BufReader::new(file); - let mut current_section = unnamed.clone(); + let mut current_section = unnamed_section.clone(); for line in reader.lines() { let line = line?; let line = line.trim_end(); - if line.trim().is_empty() { + let line_trimmed = line.trim(); + if line_trimmed.is_empty() { continue; } - if line.trim().starts_with('#') { + if line_trimmed.starts_with('#') { // It is a section name - let section = line.trim_matches(|c| c == '#' || c == ' '); - current_section = section.to_string(); + current_section = line.trim_start_matches(|c| c == '#' || c == ' ').to_string(); } else { if let Some(buffer) = entries_per_section.get_mut(¤t_section) { buffer.push('\n'); @@ -287,7 +287,7 @@ impl Release { reordered_sections.push(("".to_string(), unlabelled)); } - for section_name in [unnamed].iter().chain(config.sections.iter()) { + for section_name in [unnamed_section].iter().chain(config.sections.iter()) { if let Some(content) = entries_per_section.swap_remove(section_name) { reordered_sections.push((section_name.clone(), content)); } @@ -311,7 +311,7 @@ impl Release { buffer.push_str(&format!("\n### {}\n\n", section_name)); } buffer.push_str(content.trim_end()); - buffer.push_str("\n\n"); + buffer.push_str("\n"); } Ok(buffer) diff --git a/src/utils/empty_to_none.rs b/src/utils/empty_to_none.rs new file mode 100644 index 0000000..7c9d3ba --- /dev/null +++ b/src/utils/empty_to_none.rs @@ -0,0 +1,37 @@ + +/// Convert Option::Some() to None if the contained value is empty +pub trait EmptyToNone { + fn empty_to_none(self) -> Option; +} + +macro_rules! empty_to_none_impl { + ($ty:ty) => { + fn empty_to_none(self) -> Option<$ty> { + match self { + None => None, + Some(s) if s.is_empty() => None, + Some(s) => Some(s), + } + } + }; +} + +impl<'a> EmptyToNone<&'a str> for Option<&'a str> { + empty_to_none_impl!(&'a str); +} + +impl<'a> EmptyToNone<&'a String> for Option<&'a String> { + empty_to_none_impl!(&'a String); +} + +impl EmptyToNone for Option { + empty_to_none_impl!(String); +} + +impl EmptyToNone> for Option> { + empty_to_none_impl!(Vec); +} + +impl<'a, X> EmptyToNone<&'a Vec> for Option<&'a Vec> { + empty_to_none_impl!(&'a Vec); +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..65f47e8 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod empty_to_none; \ No newline at end of file