change default config file to include comments

cl-status
Ondřej Hruška 2 days ago
parent 0ad3cfffdf
commit d0af91c8e1
  1. 2
      src/action_init.rs
  2. 78
      src/assets/config_file_template.toml
  3. 36
      src/config.rs
  4. 5
      src/git.rs
  5. 4
      src/main.rs
  6. 16
      src/store.rs
  7. 37
      src/utils/empty_to_none.rs
  8. 1
      src/utils/mod.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: {}",

@ -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)$/'

@ -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<String>,
}

@ -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<Option<String>> {
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<Option<String>> {
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")

@ -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

@ -243,7 +243,7 @@ impl Release {
pub fn render(&self, entries_dir: impl AsRef<Path>, config: &Config) -> anyhow::Result<String> {
let mut entries_per_section = IndexMap::<String, String>::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(&current_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)

@ -0,0 +1,37 @@
/// Convert Option::Some() to None if the contained value is empty
pub trait EmptyToNone<T> {
fn empty_to_none(self) -> Option<T>;
}
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<String> for Option<String> {
empty_to_none_impl!(String);
}
impl<X> EmptyToNone<Vec<X>> for Option<Vec<X>> {
empty_to_none_impl!(Vec<X>);
}
impl<'a, X> EmptyToNone<&'a Vec<X>> for Option<&'a Vec<X>> {
empty_to_none_impl!(&'a Vec<X>);
}

@ -0,0 +1 @@
pub mod empty_to_none;
Loading…
Cancel
Save