From a3cd827cf6603a657085286952a04be550837f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 15 Sep 2025 15:49:14 +0200 Subject: [PATCH] Add format version manifest, changelog file format tweaks --- README.md | 7 ++--- src/store.rs | 76 ++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f801a87..2a439f1 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,11 @@ This tool aims to make change logging as streamlined as possible to encourage de automatically for every bugfix and feature. This can be enforced as part of a CI pipeline. The entry format is kept simple and readable, so entries can be added manually as well - e.g. if some -contributors can't or don't want to use this tool. +contributors can't or don't want to use this tool. The generated CHANGELOG.md file can be freely edited after clpack +updates it - just keep the header unchanged. -_The tool is meant to be used with Git, but it is not required - it's just more convenient, -as it can pre-fill some information from branch names._ +_clpack is meant to be used with Git, where it can pick up information from branch names. Git is, however, optional. +You can use this tool with other VCS or none at all._ ## Advantages over keeping the changelog manually diff --git a/src/store.rs b/src/store.rs index 8968fe0..ba747a1 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,18 +1,26 @@ use crate::AppContext; use crate::config::{ChannelName, Config, EntryName, VersionName}; -use anyhow::bail; +use anyhow::{Context, bail}; use colored::Colorize; use faccess::PathExt; +use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use std::borrow::Cow; -use std::fs::{OpenOptions, read_to_string}; +use std::fs::{File, OpenOptions, read_to_string}; use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; -use indexmap::IndexMap; const DIR_ENTRIES: &str = "entries"; const DIR_CHANNELS: &str = "channels"; +const SUPPORTED_FORMAT_VERSION: usize = 1; + +#[derive(Debug, Deserialize, Serialize)] +struct Manifest { + /// Versionm of the format + format_version: usize, +} + /// Changelog store struct pub struct Store<'a> { /// App context, including config @@ -30,8 +38,8 @@ impl<'a> Store<'a> { if !store_path.is_dir() { if init { // Try to create it - eprintln!("Creating changelog dir: {}", store_path.display()); - std::fs::create_dir_all(&store_path)?; + std::fs::create_dir_all(&store_path) + .with_context(|| format!("Creating changelog dir: {}", store_path.display()))?; } else { bail!( "Changelog directory does not exist: {}. Use `{} init` to create it.", @@ -48,6 +56,43 @@ impl<'a> Store<'a> { ); } + let manifest_path = store_path.join("manifest.json"); + if manifest_path.is_file() { + let manifest_file = OpenOptions::new() + .read(true) + .open(&manifest_path) + .with_context(|| format!("Opening manifest file: {}", manifest_path.display()))?; + + let manifest: Manifest = serde_json::from_reader(manifest_file) + .with_context(|| format!("Reading manifest file: {}", manifest_path.display()))?; + + if manifest.format_version != 1 { + bail!( + "clpack store is in format {}. This version of clpack requires format {}", + manifest.format_version, + SUPPORTED_FORMAT_VERSION + ); + } + } else { + println!("Creating clpack manifest file: {}", manifest_path.display()); + let manifest_file = OpenOptions::new() + .create_new(true) + .write(true) + .open(&manifest_path) + .with_context(|| { + format!( + "Opening manifest file for writing: {}", + manifest_path.display() + ) + })?; + + let manifest = Manifest { + format_version: SUPPORTED_FORMAT_VERSION, + }; + serde_json::to_writer_pretty(manifest_file, &manifest) + .with_context(|| format!("Writing manifest file: {}", manifest_path.display()))?; + } + let mut store = Self { store_path, ctx, @@ -71,8 +116,7 @@ impl<'a> Store<'a> { /// Check if a changelog entry exists. Filename is passed without extension. /// This only checks within the current epoch as older files are no longer present. pub fn entry_exists(&self, name: &str) -> bool { - let path = self.make_entry_path(name); - path.exists() + self.make_entry_path(name).exists() } /// Load release lists for all channels @@ -112,8 +156,8 @@ impl<'a> Store<'a> { subdir.display() ); } - eprintln!("Creating changelog subdir: {}", subdir.display()); - std::fs::create_dir_all(&subdir)?; + std::fs::create_dir_all(&subdir) + .with_context(|| format!("Creating subdir: {}", subdir.display()))?; } if !subdir.writable() { @@ -121,7 +165,7 @@ impl<'a> Store<'a> { } if gitkeep { - std::fs::File::create(subdir.join(".gitkeep"))?; + File::create(subdir.join(".gitkeep"))?; } Ok(()) @@ -132,9 +176,10 @@ impl<'a> Store<'a> { let path = self.make_entry_path(name.as_str()); let mut file = OpenOptions::new().write(true).create(true).open(&path)?; - eprintln!("Writing changelog entry to file: {}", path.display()); + println!("Writing changelog entry to file: {}", path.display()); - file.write_all(content.as_bytes())?; + file.write_all(content.as_bytes()) + .with_context(|| format!("Writing file {}", path.display()))?; Ok(()) } @@ -268,7 +313,9 @@ impl Release { } if line_trimmed.starts_with('#') { // It is a section name - current_section = line.trim_start_matches(|c| c == '#' || c == ' ').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'); @@ -308,12 +355,13 @@ impl Release { for (section_name, content) in reordered_sections { if !section_name.is_empty() { - buffer.push_str(&format!("\n### {}\n\n", section_name)); + buffer.push_str(&format!("\n### {}\n", section_name)); } buffer.push_str(content.trim_end()); buffer.push_str("\n"); } + buffer.push_str("\n"); Ok(buffer) } }