From ba8f25361d1a29eedcf79c74d329f69b0c1c5082 Mon Sep 17 00:00:00 2001 From: Zachary Vander Velden <46034847+exzachlyvv@users.noreply.github.com> Date: Thu, 7 Nov 2024 03:53:54 -0600 Subject: [PATCH] Implement changelog update functionality. (#646) 2 notes: * This PR correctly writes back to Cargo.toml file however the original format is not preserved. Any easy way to do that? * Not yet handling updates to crates that depend on originated change. That is next. --- crates/cli-tools/src/changelog.rs | 154 +++++++++++++++++++++++++++++- crates/xtask/src/main.rs | 2 +- 2 files changed, 150 insertions(+), 6 deletions(-) diff --git a/crates/cli-tools/src/changelog.rs b/crates/cli-tools/src/changelog.rs index 5b717dd3..54ab0b85 100644 --- a/crates/cli-tools/src/changelog.rs +++ b/crates/cli-tools/src/changelog.rs @@ -24,7 +24,7 @@ use std::io::BufRead; use anyhow::{anyhow, bail, ensure, Context, Result}; use clap::ValueEnum; -use semver::Version; +use semver::{Prerelease, Version}; use tokio::process::Command; use crate::cargo::metadata; @@ -58,6 +58,38 @@ impl Changelog { Self::parse(&String::from_utf8(fs::read(path).await?)?) } + async fn write_file(&self, path: &str) -> Result<()> { + fs::write(path, self.to_string().as_bytes()).await + } + + fn push_description(&mut self, severity: Severity, content: &str) -> Result<()> { + let content = if content.starts_with("- ") { content } else { &format!("- {content}") }; + let current_release = self.get_or_create_release_mut(); + current_release.push_content(severity, content) + } + + // Gets newest (first) release. Creates a new one if newest release is not prerelease. + fn get_or_create_release_mut(&mut self) -> &mut Release { + let current_release = self.releases.first().unwrap(); + + // Current version is released, insert a new one. + if current_release.version.pre.is_empty() { + let mut next_version = Version::new( + current_release.version.major, + current_release.version.minor, + current_release.version.patch + 1, + ); + + next_version.pre = Prerelease::new("git").unwrap(); + + let new_release = Release::from(next_version); + + self.releases.insert(0, new_release); + } + + self.releases.first_mut().unwrap() + } + /// Parses and validates a changelog. fn parse(input: &str) -> Result { let mut releases: Vec = Vec::new(); @@ -147,6 +179,24 @@ impl Changelog { ); Ok(()) } + + async fn sync_cargo_toml(&self, path: &str) -> Result<()> { + let expected_version = &self.releases.first().unwrap().version; + + let mut sed = Command::new("sed"); + sed.arg("-i"); + sed.arg(format!("s#^version = .*#version = \"{expected_version}\"#")); + sed.arg("Cargo.toml"); + cmd::execute(sed.current_dir(path)).await?; + + Ok(()) + } + + async fn sync_dependencies(&self, _path: &str) -> Result<()> { + // TODO + + Ok(()) + } } impl Display for Changelog { @@ -209,6 +259,19 @@ struct Release { contents: BTreeMap>, } +impl Release { + fn push_content(&mut self, severity: Severity, content: &str) -> Result<()> { + self.contents.entry(severity).or_default().push(content.to_string()); + Ok(()) + } +} + +impl From for Release { + fn from(version: Version) -> Self { + Release { version, contents: BTreeMap::new() } + } +} + impl Display for Release { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "## {}\n", self.version)?; @@ -260,12 +323,17 @@ pub async fn execute_ci() -> Result<()> { } /// Updates a changelog file and changelog files of dependencies. -pub async fn execute_change(path: &str, _severity: &Severity, _description: &str) -> Result<()> { - ensure!(fs::exists(path).await, "Crate does not exist: {path}"); +pub async fn execute_change(path: &str, severity: Severity, description: &str) -> Result<()> { + let changelog_file_path = format!("{path}/CHANGELOG.md"); + + let mut changelog = Changelog::read_file(&changelog_file_path).await?; - let _changelog = Changelog::read_file(&format!("{path}/CHANGELOG.md")).await?; + changelog.push_description(severity, description)?; + changelog.write_file(&changelog_file_path).await?; + changelog.sync_cargo_toml(path).await?; + changelog.sync_dependencies(path).await?; - todo!("Implement changelog updates"); + Ok(()) } #[cfg(test)] @@ -717,4 +785,80 @@ mod tests { "Unexpected prerelease line 9" ); } + + #[test] + fn push_description_prepends_dash_only_when_needed() { + let changelog_str = r"# Changelog + +## 0.2.0-git + +### Major + +- A change + +## 0.1.0 + + +"; + + let mut changelog = Changelog::parse(changelog_str).expect("Failed to parse changelog."); + + changelog.push_description(Severity::Major, "testing no dash").unwrap(); + changelog.push_description(Severity::Major, "- testing with dash").unwrap(); + + assert_eq!( + changelog.get_or_create_release_mut().contents.get(&Severity::Major).unwrap(), + &vec![ + "- A change".to_string(), + "- testing no dash".to_string(), + "- testing with dash".to_string(), + ] + ); + } + + #[test] + fn get_or_create_release_mut_uses_first_when_prerelease() { + let changelog_str = r"# Changelog + +## 0.2.0-git + +### Major + +- A change + +## 0.1.0 + + +"; + + let mut changelog = Changelog::parse(changelog_str).expect("Failed to parse changelog."); + + let current_release = changelog.get_or_create_release_mut(); + + assert_eq!(current_release.version, Version::parse("0.2.0-git").unwrap()); + } + + #[test] + fn get_or_create_release_mut_creates_new_when_current_is_released() { + let changelog_str = r"# Changelog + +## 0.2.0 + +### Major + +- A change + +## 0.1.0 + + +"; + + let mut changelog = Changelog::parse(changelog_str).expect("Failed to parse changelog."); + + let current_release = changelog.get_or_create_release_mut(); + + assert_eq!(current_release.version, Version::parse("0.2.1-git").unwrap()); + // Assert the new release already inserted + assert_eq!(changelog.releases.len(), 3); + } } diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index fe66b25f..24c96e6f 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -299,7 +299,7 @@ impl Flags { MainCommand::Changelog(subcommand) => match subcommand.command { ChangelogCommand::Ci => changelog::execute_ci().await, ChangelogCommand::Change { path, severity, description } => { - changelog::execute_change(&path, &severity, &description).await + changelog::execute_change(&path, severity, &description).await } }, }