feat: with prepend as well
Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
parent
86664bc497
commit
8c3a0c699c
25
Cargo.lock
generated
25
Cargo.lock
generated
@ -346,6 +346,8 @@ dependencies = [
|
|||||||
"conventional_commit_parser",
|
"conventional_commit_parser",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"git-cliff-core",
|
"git-cliff-core",
|
||||||
|
"lazy_static",
|
||||||
|
"parse-changelog",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@ -815,6 +817,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.14.0",
|
"hashbrown 0.14.0",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -887,6 +890,12 @@ version = "1.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lexopt"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baff4b617f7df3d896f97fe922b64817f6cd9a756bb81d40f8883f2f66dcb401"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.147"
|
version = "0.2.147"
|
||||||
@ -1110,6 +1119,22 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parse-changelog"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39a24196a65fc15a0a747df8c041abc5a009f2c09c550b0a14f7eeb0c10255ef"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"indexmap 2.0.0",
|
||||||
|
"lexopt",
|
||||||
|
"memchr",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parse-zoneinfo"
|
name = "parse-zoneinfo"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
@ -20,6 +20,8 @@ reqwest = { version = "0.11.18" }
|
|||||||
git-cliff-core = "1.2.0"
|
git-cliff-core = "1.2.0"
|
||||||
regex = "*"
|
regex = "*"
|
||||||
chrono = "*"
|
chrono = "*"
|
||||||
|
lazy_static = "*"
|
||||||
|
parse-changelog = "*"
|
||||||
|
|
||||||
tracing-test = "0.2"
|
tracing-test = "0.2"
|
||||||
pretty_assertions = "1.4"
|
pretty_assertions = "1.4"
|
||||||
|
@ -19,6 +19,8 @@ tempdir.workspace = true
|
|||||||
git-cliff-core.workspace = true
|
git-cliff-core.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
parse-changelog.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tracing-test = { workspace = true, features = ["no-env-filter"] }
|
tracing-test = { workspace = true, features = ["no-env-filter"] }
|
||||||
|
@ -107,6 +107,31 @@ impl ChangeLog<'_> {
|
|||||||
.context("cannot convert bytes to string (contains non utf-8 char indices)")
|
.context("cannot convert bytes to string (contains non utf-8 char indices)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prepend(self, old_changelog: impl Into<String>) -> anyhow::Result<String> {
|
||||||
|
let old_changelog = old_changelog.into();
|
||||||
|
if let Ok(Some(last_version)) = changelog_parser::last_version_from_str(&old_changelog) {
|
||||||
|
let next_version = self
|
||||||
|
.release
|
||||||
|
.version
|
||||||
|
.as_ref()
|
||||||
|
.context("current release contains no version")?;
|
||||||
|
if next_version == &last_version {
|
||||||
|
return Ok(old_changelog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let old_header = changelog_parser::parse_header(&old_changelog);
|
||||||
|
let config = self
|
||||||
|
.config
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| self.default_config_with_header(old_header));
|
||||||
|
let changelog = Changelog::new(vec![self.release], &config)?;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
changelog.prepend(old_changelog, &mut out)?;
|
||||||
|
String::from_utf8(out)
|
||||||
|
.context("cannot convert bytes to string (contains non utf-8 char indices)")
|
||||||
|
}
|
||||||
|
|
||||||
fn default_config<'a>(&self) -> Config {
|
fn default_config<'a>(&self) -> Config {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
changelog: default_changelog_config(
|
changelog: default_changelog_config(
|
||||||
@ -118,6 +143,18 @@ impl ChangeLog<'_> {
|
|||||||
|
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_config_with_header<'a>(&self, header: Option<String>) -> Config {
|
||||||
|
let config = Config {
|
||||||
|
changelog: default_changelog_config(
|
||||||
|
header,
|
||||||
|
self.release_link.as_ref().map(|rl| rl.as_str()),
|
||||||
|
),
|
||||||
|
git: default_git_config(),
|
||||||
|
};
|
||||||
|
|
||||||
|
config
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_git_config() -> GitConfig {
|
fn default_git_config() -> GitConfig {
|
||||||
@ -199,6 +236,245 @@ fn default_changelog_body_config(release_link: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod changelog_parser {
|
||||||
|
use std::{fs::read_to_string, path::Path};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
/// Parse the header from a changelog.
|
||||||
|
/// The changelog header is a string at the begin of the changelog that:
|
||||||
|
/// - Starts with `# Changelog`, `# CHANGELOG`, or `# changelog`
|
||||||
|
/// - ends with `## Unreleased`, `## [Unreleased]` or `## ..anything..`
|
||||||
|
/// (in the ..anything.. case, `## ..anything..` is not included in the header)
|
||||||
|
pub fn parse_header(changelog: &str) -> Option<String> {
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref FIRST_RE: Regex = Regex::new(r#"(?s)^(# Changelog|# CHANGELOG|# changelog)(.*)(## Unreleased|## \[Unreleased\])"#).unwrap();
|
||||||
|
|
||||||
|
static ref SECOND_RE: Regex = Regex::new(r#"(?s)^(# Changelog|# CHANGELOG|# changelog)(.*)(\n## )"#).unwrap();
|
||||||
|
}
|
||||||
|
if let Some(captures) = FIRST_RE.captures(changelog) {
|
||||||
|
return Some(format!("{}\n", &captures[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(captures) = SECOND_RE.captures(changelog) {
|
||||||
|
return Some(format!("{}{}", &captures[1], &captures[2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last_changes(changelog: &Path) -> anyhow::Result<Option<String>> {
|
||||||
|
let changelog = read_to_string(changelog).context("can't read changelog file")?;
|
||||||
|
last_changes_from_str(&changelog)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last_changes_from_str(changelog: &str) -> anyhow::Result<Option<String>> {
|
||||||
|
let parser = ChangelogParser::new(changelog)?;
|
||||||
|
let last_release = parser.last_release().map(|r| r.notes.to_string());
|
||||||
|
Ok(last_release)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last_version_from_str(changelog: &str) -> anyhow::Result<Option<String>> {
|
||||||
|
let parser = ChangelogParser::new(changelog)?;
|
||||||
|
let last_release = parser.last_release().map(|r| r.version.to_string());
|
||||||
|
Ok(last_release)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last_release_from_str(changelog: &str) -> anyhow::Result<Option<ChangelogRelease>> {
|
||||||
|
let parser = ChangelogParser::new(changelog)?;
|
||||||
|
let last_release = parser.last_release().map(ChangelogRelease::from_release);
|
||||||
|
Ok(last_release)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChangelogRelease {
|
||||||
|
title: String,
|
||||||
|
notes: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChangelogRelease {
|
||||||
|
fn from_release(release: &parse_changelog::Release) -> Self {
|
||||||
|
Self {
|
||||||
|
title: release.title.to_string(),
|
||||||
|
notes: release.notes.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> &str {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notes(&self) -> &str {
|
||||||
|
&self.notes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChangelogParser<'a> {
|
||||||
|
changelog: parse_changelog::Changelog<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ChangelogParser<'a> {
|
||||||
|
pub fn new(changelog_text: &'a str) -> anyhow::Result<Self> {
|
||||||
|
let changelog =
|
||||||
|
parse_changelog::parse(changelog_text).context("can't parse changelog")?;
|
||||||
|
Ok(Self { changelog })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn last_release(&self) -> Option<&parse_changelog::Release> {
|
||||||
|
let last_release = release_at(&self.changelog, 0)?;
|
||||||
|
let last_release = if last_release.version.to_lowercase().contains("unreleased") {
|
||||||
|
release_at(&self.changelog, 1)?
|
||||||
|
} else {
|
||||||
|
last_release
|
||||||
|
};
|
||||||
|
Some(last_release)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release_at<'a>(
|
||||||
|
changelog: &'a parse_changelog::Changelog,
|
||||||
|
index: usize,
|
||||||
|
) -> Option<&'a parse_changelog::Release<'a>> {
|
||||||
|
let release = changelog.get_index(index)?.1;
|
||||||
|
Some(release)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn last_changes_from_str_test(changelog: &str) -> String {
|
||||||
|
last_changes_from_str(changelog).unwrap().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changelog_header_is_parsed() {
|
||||||
|
let changelog = "\
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
My custom changelog header
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
";
|
||||||
|
let header = parse_header(changelog).unwrap();
|
||||||
|
let expected_header = "\
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
My custom changelog header
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
";
|
||||||
|
assert_eq!(header, expected_header);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changelog_header_without_unreleased_is_parsed() {
|
||||||
|
let changelog = "\
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
My custom changelog header
|
||||||
|
|
||||||
|
## [0.2.5] - 2022-12-16
|
||||||
|
";
|
||||||
|
let header = parse_header(changelog).unwrap();
|
||||||
|
let expected_header = "\
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
My custom changelog header
|
||||||
|
";
|
||||||
|
assert_eq!(header, expected_header);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changelog_header_with_versions_is_parsed() {
|
||||||
|
let changelog = "\
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
My custom changelog header
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.2.5] - 2022-12-16
|
||||||
|
";
|
||||||
|
let header = parse_header(changelog).unwrap();
|
||||||
|
let expected_header = "\
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
My custom changelog header
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
";
|
||||||
|
assert_eq!(header, expected_header);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changelog_header_isnt_recognized() {
|
||||||
|
// A two-level header similar to `## [Unreleased]` is missing
|
||||||
|
let changelog = "\
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
My custom changelog header
|
||||||
|
";
|
||||||
|
let header = parse_header(changelog);
|
||||||
|
assert_eq!(header, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changelog_with_unreleased_section_is_parsed() {
|
||||||
|
let changelog = "\
|
||||||
|
# Changelog
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.2.5] - 2022-12-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add function to retrieve default branch (#372)
|
||||||
|
|
||||||
|
## [0.2.4] - 2022-12-12
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- improved error message
|
||||||
|
";
|
||||||
|
let changes = last_changes_from_str_test(changelog);
|
||||||
|
let expected_changes = "\
|
||||||
|
### Added
|
||||||
|
- Add function to retrieve default branch (#372)";
|
||||||
|
assert_eq!(changes, expected_changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changelog_without_unreleased_section_is_parsed() {
|
||||||
|
let changelog = "\
|
||||||
|
# Changelog
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.2.5](https://github.com/MarcoIeni/release-plz/compare/git_cmd-v0.2.4...git_cmd-v0.2.5) - 2022-12-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add function to retrieve default branch (#372)
|
||||||
|
|
||||||
|
## [0.2.4] - 2022-12-12
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- improved error message
|
||||||
|
";
|
||||||
|
let changes = last_changes_from_str_test(changelog);
|
||||||
|
let expected_changes = "\
|
||||||
|
### Added
|
||||||
|
- Add function to retrieve default branch (#372)";
|
||||||
|
assert_eq!(changes, expected_changes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
Loading…
Reference in New Issue
Block a user