implement changelog packing, more config options, better formatting & flexibility

cl-status
Ondřej Hruška 3 days ago
parent e6ff1520ee
commit 55dc83adc6
  1. 226
      Cargo.lock
  2. 1
      Cargo.toml
  3. 79
      src/action_pack.rs
  4. 11
      src/config.rs
  5. 4
      src/git.rs
  6. 24
      src/main.rs
  7. 78
      src/store.rs

226
Cargo.lock generated

@ -11,6 +11,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.20" version = "0.6.20"
@ -85,18 +94,47 @@ version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.3" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "chrono"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link 0.2.0",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.47" version = "4.5.47"
@ -129,6 +167,7 @@ name = "clpack"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono",
"clap", "clap",
"colored", "colored",
"faccess", "faccess",
@ -156,6 +195,12 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "crossterm" name = "crossterm"
version = "0.25.0" version = "0.25.0"
@ -220,6 +265,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "find-msvc-tools"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
[[package]] [[package]]
name = "fuzzy-matcher" name = "fuzzy-matcher"
version = "0.3.7" version = "0.3.7"
@ -256,6 +307,30 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
[[package]]
name = "iana-time-zone"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.11.1" version = "2.11.1"
@ -296,6 +371,16 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.175" version = "0.2.175"
@ -351,6 +436,15 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.3"
@ -461,6 +555,12 @@ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.20" version = "1.0.20"
@ -514,6 +614,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.18" version = "0.3.18"
@ -707,6 +813,65 @@ dependencies = [
"wit-bindgen", "wit-bindgen",
] ]
[[package]]
name = "wasm-bindgen"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -729,12 +894,71 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.62.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.2.0",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.1.3" version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-result"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
dependencies = [
"windows-link 0.2.0",
]
[[package]]
name = "windows-strings"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
dependencies = [
"windows-link 0.2.0",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"
@ -790,7 +1014,7 @@ version = "0.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
dependencies = [ dependencies = [
"windows-link", "windows-link 0.1.3",
"windows_aarch64_gnullvm 0.53.0", "windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0", "windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0", "windows_i686_gnu 0.53.0",

@ -16,6 +16,7 @@ regex = "1"
anyhow = "1" anyhow = "1"
colored = "3" colored = "3"
faccess = "0.2" faccess = "0.2"
chrono = "0.4"
# input # input

@ -1,19 +1,32 @@
use crate::AppContext; use crate::AppContext;
use crate::config::ChannelName;
use crate::git::get_branch_name; use crate::git::get_branch_name;
use crate::store::{Release, Store}; use crate::store::{Release, Store};
use anyhow::bail; use anyhow::bail;
use colored::Colorize; use colored::Colorize;
/// Perform the action of packing changelog entries for a release /// Perform the action of packing changelog entries for a release
pub(crate) fn cl_pack(ctx: AppContext) -> anyhow::Result<()> { pub(crate) fn cl_pack(ctx: AppContext, channel: Option<ChannelName>) -> anyhow::Result<()> {
let mut store = Store::new(&ctx, false)?; let mut store = Store::new(&ctx, false)?;
let branch = get_branch_name(&ctx); let branch = get_branch_name(&ctx);
let channel_detected = branch let (channel_detected, channel_explicit) = match channel {
.as_ref() Some(ch) => (Some(ch), true), // passed via flag already
.map(|b| b.parse_channel(&ctx)) None => (
.transpose()? branch
.flatten(); .as_ref()
.map(|b| b.parse_channel(&ctx))
.transpose()?
.flatten(),
false,
),
};
if let Some(ch) = &channel_detected
&& !ctx.config.channels.contains_key(ch)
{
bail!("No such channel: {ch}");
}
// If the branch is named rel/3.40, this can extract 3.40. // If the branch is named rel/3.40, this can extract 3.40.
// TODO try to get something better from git! // TODO try to get something better from git!
@ -33,16 +46,20 @@ pub(crate) fn cl_pack(ctx: AppContext) -> anyhow::Result<()> {
// Ask for the channel // Ask for the channel
let channel = if ctx.config.channels.len() > 1 { let channel = if ctx.config.channels.len() > 1 {
let channels = ctx.config.channels.values().collect::<Vec<_>>(); if channel_explicit {
let mut starting_index = None; channel_detected.unwrap()
if let Some(channel) = channel_detected { } else {
starting_index = channels.iter().position(|ch| *ch == &channel); let channels = ctx.config.channels.keys().collect::<Vec<_>>();
} let mut starting_index = None;
let mut query = inquire::Select::new("Release channel?", channels); if let Some(channel) = channel_detected {
if let Some(index) = starting_index { starting_index = channels.iter().position(|ch| *ch == &channel);
query = query.with_starting_cursor(index); }
let mut query = inquire::Select::new("Release channel?", channels);
if let Some(index) = starting_index {
query = query.with_starting_cursor(index);
}
query.prompt()?.to_string()
} }
query.prompt()?.to_string()
} else { } else {
// Just one channel, so use that // Just one channel, so use that
ctx.config.default_channel.clone() ctx.config.default_channel.clone()
@ -57,9 +74,9 @@ pub(crate) fn cl_pack(ctx: AppContext) -> anyhow::Result<()> {
} }
println!(); println!();
println!("Changes waiting for release:\n"); println!("Changes waiting for release:");
for entry in &unreleased { for entry in &unreleased {
println!("+ {entry}\n"); println!("+ {}", entry.cyan());
} }
println!(); println!();
@ -82,11 +99,25 @@ pub(crate) fn cl_pack(ctx: AppContext) -> anyhow::Result<()> {
} }
} }
store.create_release( let release = Release {
channel, version,
Release { entries: unreleased,
version, };
entries: unreleased,
}, let rendered = store.render_release(&release)?;
)
println!("\n\nPreview:\n\n{}\n", rendered);
if !inquire::Confirm::new("Continue - write to changelog file?")
.with_default(true)
.prompt()?
{
eprintln!("{}", "Cancelled.".red());
return Ok(());
}
store.create_release(channel, release)?;
println!("{}", "Changelog written.".green());
Ok(())
} }

@ -37,10 +37,19 @@ pub struct Config {
/// - `{channel}`, `{Channel}`, `{CHANNEL}` - Channel ID in the respective capitalization /// - `{channel}`, `{Channel}`, `{CHANNEL}` - Channel ID in the respective capitalization
#[default = "CHANGELOG-{CHANNEL}.md"] #[default = "CHANGELOG-{CHANNEL}.md"]
pub changelog_file_channel: String, pub changelog_file_channel: String,
/// Title of the changelog file, stripped and put back in front
#[default = "# Changelog\n\n"] #[default = "# Changelog\n\n"]
pub changelog_header: String, pub changelog_header: String,
/// Pattern for release header
#[default = "[{VERSION}] - {DATE}"]
pub release_header: String,
/// Date format (see patterns supported by the Chrono crate: https://docs.rs/chrono/latest/chrono/format/strftime/index.html )
#[default = "%Y-%m-%d"]
pub date_format: String,
/// Changelog sections suggested when creating a new entry. /// Changelog sections suggested when creating a new entry.
/// ///
/// Users may also specify a custom section name. /// Users may also specify a custom section name.

@ -98,6 +98,10 @@ impl BranchName {
/// Try to detect a release channel from this branch name (e.g. stable, EAP) /// Try to detect a release channel from this branch name (e.g. stable, EAP)
pub fn parse_channel(&self, ctx: &AppContext) -> anyhow::Result<Option<String>> { pub fn parse_channel(&self, ctx: &AppContext) -> anyhow::Result<Option<String>> {
for (channel_id, template) in &ctx.config.channels { for (channel_id, template) in &ctx.config.channels {
if template.is_empty() {
// Channel only for manual choosing
continue;
}
if let Some(pat_s) = as_regex_pattern(template) { if let Some(pat_s) = as_regex_pattern(template) {
let pat = match regex::Regex::new(pat_s) { let pat = match regex::Regex::new(pat_s) {
Ok(pat) => pat, Ok(pat) => pat,

@ -1,7 +1,7 @@
use crate::action_init::{ClInit, cl_init}; use crate::action_init::{ClInit, cl_init};
use crate::action_log::cl_log; use crate::action_log::cl_log;
use crate::action_pack::cl_pack; use crate::action_pack::cl_pack;
use crate::config::Config; use crate::config::{ChannelName, Config};
use anyhow::bail; use anyhow::bail;
use clap::builder::NonEmptyStringValueParser; use clap::builder::NonEmptyStringValueParser;
use colored::Colorize; use colored::Colorize;
@ -33,7 +33,7 @@ pub struct AppContext {
fn main() { fn main() {
if let Err(e) = main_try() { if let Err(e) = main_try() {
eprintln!("{}", e.to_string().red().bold()); eprintln!("{}", format!("{:?}", e).red().bold());
exit(1); exit(1);
} }
} }
@ -54,15 +54,20 @@ fn main_try() -> anyhow::Result<()> {
.subcommand( .subcommand(
clap::Command::new("pack") clap::Command::new("pack")
.visible_alias("release") .visible_alias("release")
.about("Pack changelog entries to a changelog section"), .about("Pack changelog entries to a changelog section")
.arg(clap::Arg::new("CHANNEL")
.short('x')
.long("channel")
.value_parser(NonEmptyStringValueParser::new())
.required(false)),
) )
.subcommand(clap::Command::new("add") .subcommand(clap::Command::new("add")
.visible_alias("log") .visible_alias("log")
.about("Add a changelog entry on the current branch")) .about("Add a changelog entry on the current branch"))
.subcommand(clap::Command::new("flush") // .subcommand(clap::Command::new("flush")
.about("Remove all changelog entries that were already released on all channels - clean up the changelog dir. Use e.g. when making a major release where all channel branches are merged.")) // .about("Remove all changelog entries that were already released on all channels - clean up the changelog dir. Use e.g. when making a major release where all channel branches are merged."))
.subcommand(clap::Command::new("status") // .subcommand(clap::Command::new("status")
.about("Show changelog entries currently waiting for release on the current channel")) // .about("Show changelog entries currently waiting for release on the current channel"))
.subcommand_required(false) .subcommand_required(false)
.arg(clap::Arg::new("CONFIG") .arg(clap::Arg::new("CONFIG")
.short('c') .short('c')
@ -122,8 +127,9 @@ fn main_try() -> anyhow::Result<()> {
// eprintln!("AppCtx: {:?}", ctx); // eprintln!("AppCtx: {:?}", ctx);
match args.subcommand() { match args.subcommand() {
Some(("pack", _)) => { Some(("pack", subargs)) => {
cl_pack(ctx)?; let channel: Option<ChannelName> = subargs.get_one("CHANNEL").cloned();
cl_pack(ctx, channel)?;
} }
None | Some(("add", _)) => cl_log(ctx)?, None | Some(("add", _)) => cl_log(ctx)?,
// TODO: status, flush // TODO: status, flush

@ -1,5 +1,5 @@
use crate::AppContext; use crate::AppContext;
use crate::config::{ChannelName, EntryName, VersionName}; use crate::config::{ChannelName, Config, EntryName, VersionName};
use anyhow::bail; use anyhow::bail;
use colored::Colorize; use colored::Colorize;
use faccess::PathExt; use faccess::PathExt;
@ -92,8 +92,8 @@ impl<'a> Store<'a> {
/// Check and create internal subdirs for the clpack system /// Check and create internal subdirs for the clpack system
pub fn ensure_internal_subdirs_exist(&self) -> anyhow::Result<()> { pub fn ensure_internal_subdirs_exist(&self) -> anyhow::Result<()> {
self.ensure_subdir_exists(DIR_ENTRIES)?; self.ensure_subdir_exists(DIR_ENTRIES, true)?;
self.ensure_subdir_exists(DIR_CHANNELS)?; self.ensure_subdir_exists(DIR_CHANNELS, false)?;
// TODO // TODO
Ok(()) Ok(())
@ -102,7 +102,7 @@ impl<'a> Store<'a> {
/// make sure a subdir exists, creating if needed. /// make sure a subdir exists, creating if needed.
/// ///
/// Note there is no lock so there can be a TOCTOU bug later if someone deletes the path - must be checked and handled. /// Note there is no lock so there can be a TOCTOU bug later if someone deletes the path - must be checked and handled.
fn ensure_subdir_exists(&self, name: &str) -> anyhow::Result<()> { fn ensure_subdir_exists(&self, name: &str, gitkeep: bool) -> anyhow::Result<()> {
let subdir = self.store_path.join(name); let subdir = self.store_path.join(name);
if !subdir.is_dir() { if !subdir.is_dir() {
@ -120,7 +120,9 @@ impl<'a> Store<'a> {
bail!("Changelog subdir is not writable: {}", subdir.display()); bail!("Changelog subdir is not writable: {}", subdir.display());
} }
std::fs::File::create(subdir.join(".gitkeep"))?; if gitkeep {
std::fs::File::create(subdir.join(".gitkeep"))?;
}
Ok(()) Ok(())
} }
@ -157,12 +159,13 @@ impl<'a> Store<'a> {
/// Create a release entry, write it to the releases buffer and to the file. /// Create a release entry, write it to the releases buffer and to the file.
pub fn create_release(&mut self, channel: ChannelName, release: Release) -> anyhow::Result<()> { pub fn create_release(&mut self, channel: ChannelName, release: Release) -> anyhow::Result<()> {
let rendered = self.render_release(&release)?;
let Some(store) = self.versions.get_mut(&channel) else { let Some(store) = self.versions.get_mut(&channel) else {
bail!("Channel {channel} does not exist."); bail!("Channel {channel} does not exist.");
}; };
let config = &self.ctx.config; let config = &self.ctx.config;
let rendered = release.render(self.store_path.join(DIR_ENTRIES), &config.sections)?;
let changelog_file = self.ctx.root.join( let changelog_file = self.ctx.root.join(
if channel == config.default_channel { if channel == config.default_channel {
@ -209,6 +212,12 @@ impl<'a> Store<'a> {
store.write_to_file()?; store.write_to_file()?;
Ok(()) Ok(())
} }
/// Render a release
pub fn render_release(&self, release: &Release) -> anyhow::Result<String> {
let config = &self.ctx.config;
release.render(self.store_path.join(DIR_ENTRIES), &config)
}
} }
/// Uppercase first char of a string /// Uppercase first char of a string
@ -230,12 +239,8 @@ pub struct Release {
} }
impl Release { impl Release {
/// Render the entry into a Markdown fragment, using h2 (##) as the title. /// Render the entry into a Markdown fragment, using h2 (##) as the title, h3 (###) for sections
pub fn render( pub fn render(&self, entries_dir: impl AsRef<Path>, config: &Config) -> anyhow::Result<String> {
&self,
entries_dir: impl AsRef<Path>,
predefined_sections: &[String],
) -> anyhow::Result<String> {
let mut entries_per_section = HashMap::<String, String>::new(); let mut entries_per_section = HashMap::<String, String>::new();
let entries_dir = entries_dir.as_ref(); let entries_dir = entries_dir.as_ref();
let unnamed = "".to_string(); let unnamed = "".to_string();
@ -256,6 +261,9 @@ impl Release {
let mut current_section = unnamed.clone(); let mut current_section = unnamed.clone();
for line in reader.lines() { for line in reader.lines() {
let line = line?; let line = line?;
if line.trim().is_empty() {
continue;
}
if line.trim().starts_with('#') { if line.trim().starts_with('#') {
// It is a section name // It is a section name
let section = line.trim_matches(|c| c == '#' || c == ' '); let section = line.trim_matches(|c| c == '#' || c == ' ');
@ -278,7 +286,7 @@ impl Release {
reordered_sections.push(("".to_string(), unlabelled)); reordered_sections.push(("".to_string(), unlabelled));
} }
for section_name in [unnamed].iter().chain(predefined_sections.iter()) { for section_name in [unnamed].iter().chain(config.sections.iter()) {
if let Some(content) = entries_per_section.remove(section_name) { if let Some(content) = entries_per_section.remove(section_name) {
reordered_sections.push((section_name.clone(), content)); reordered_sections.push((section_name.clone(), content));
} }
@ -288,14 +296,21 @@ impl Release {
reordered_sections.push((section_name, content)); reordered_sections.push((section_name, content));
} }
let mut buffer = String::new(); let date = chrono::Local::now();
let mut buffer = format!(
"## {}\n",
config
.release_header
.replace("{VERSION}", &self.version)
.replace("{DATE}", &date.format(&config.date_format).to_string())
);
for (section_name, content) in reordered_sections { for (section_name, content) in reordered_sections {
if !section_name.is_empty() { if !section_name.is_empty() {
buffer.push_str(&format!("## {}\n", section_name)); buffer.push_str(&format!("\n### {}\n\n", section_name));
} }
buffer.push_str(&content); buffer.push_str(content.trim_end());
buffer.push('\n'); buffer.push_str("\n\n");
} }
Ok(buffer) Ok(buffer)
@ -318,13 +333,18 @@ struct ChannelReleaseStore {
impl ChannelReleaseStore { impl ChannelReleaseStore {
/// Load from a versions file /// Load from a versions file
fn load(releases_file: PathBuf, channel_name: ChannelName) -> anyhow::Result<Self> { fn load(releases_file: PathBuf, channel_name: ChannelName) -> anyhow::Result<Self> {
println!(
"Loading versions for channel {} from: {}",
channel_name,
releases_file.display()
);
let releases = if !releases_file.exists() { let releases = if !releases_file.exists() {
// File did not exist yet, create it // File did not exist yet, create it - this catches error with write access early
let mut f = OpenOptions::new() let mut f = OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
.open(&releases_file)?; .open(&releases_file)?;
f.write_all("{}".as_bytes())?; f.write_all("[]".as_bytes())?;
Default::default() Default::default()
} else { } else {
let channel_json = read_to_string(&releases_file)?; let channel_json = read_to_string(&releases_file)?;
@ -384,14 +404,16 @@ impl ChannelReleaseStore {
})?; })?;
if !entry.metadata()?.is_file() || !fname.ends_with(".md") { if !entry.metadata()?.is_file() || !fname.ends_with(".md") {
eprintln!( if fname != ".gitkeep" {
"{}", eprintln!(
format!( "{}",
"Unexpected item in changelog entries dir: {}", format!(
entry.path().display() "Unexpected item in changelog entries dir: {}",
) entry.path().display()
.yellow() )
); .yellow()
);
}
continue; continue;
} }
@ -404,7 +426,7 @@ impl ChannelReleaseStore {
.flatten() .flatten()
.any(|entryname| entryname == basename) .any(|entryname| entryname == basename)
{ {
found.push(fname); found.push(basename.to_string());
} }
} }

Loading…
Cancel
Save