From ae8dd8c6e5c419f91b9a9ca4e270862b18c5c599 Mon Sep 17 00:00:00 2001 From: Toby Vincent Date: Sun, 4 Jun 2023 14:15:51 -0500 Subject: build: improve ergonomics of version replace --- xtask/src/release.rs | 98 +++++++------- xtask/src/release/bump.rs | 290 +++++++++++++++++++++++++++++++++++++++++ xtask/src/release/version.rs | 302 ------------------------------------------- 3 files changed, 337 insertions(+), 353 deletions(-) create mode 100644 xtask/src/release/bump.rs delete mode 100644 xtask/src/release/version.rs (limited to 'xtask') diff --git a/xtask/src/release.rs b/xtask/src/release.rs index 8bf4444..acb8888 100644 --- a/xtask/src/release.rs +++ b/xtask/src/release.rs @@ -4,9 +4,9 @@ use anyhow::Result; use clap::{Args, Subcommand}; use semver::Version; -use self::version::{Bump, Level, Replacement}; +use self::bump::{Bump, Level}; -mod version; +mod bump; #[derive(Debug, Clone, Args)] pub struct Release { @@ -15,7 +15,7 @@ pub struct Release { /// Level of version bump version. #[arg(global = true, required = false)] - level: version::Level, + level: bump::Level, /// Options passed to git commit. #[arg(global = true, last = true)] @@ -42,7 +42,7 @@ pub enum Step { /// Bump version in package files and commit changes. Bump { #[arg(from_global)] - level: version::Level, + level: bump::Level, }, /// Make a release commit. @@ -54,7 +54,7 @@ pub enum Step { /// Create git tag for release. Tag { #[arg(from_global)] - level: version::Level, + level: bump::Level, }, } @@ -85,54 +85,50 @@ impl Step { pub fn bump(level: Level) -> Result { let mut bump = Bump::from(level); - bump.bump_file("README.md", &[Replacement::Version])?; - bump.bump_file("pkg/archlinux/projectr/PKGBUILD", &[Replacement::Version])?; - bump.bump_file( - "pkg/archlinux/projectr-bin/PKGBUILD", - &[Replacement::Version], - )?; - - let stdout = std::process::Command::new("git") - .arg("describe") - .arg("--long") - .arg("--abbrev=7") - .output()? - .stdout; - let pkgver = std::str::from_utf8(&stdout)? - .trim() - .trim_start_matches('v') - .replacen("-g", ".g", 1) - .replacen('-', "-r", 1) - .replace('-', "."); - - bump.bump_file( - "pkg/archlinux/projectr-git/PKGBUILD", - &[Replacement::FindLine( - |l| l.starts_with("pkgver="), - format!("pkgver={pkgver}"), - )], - )?; - - bump.bump_file( - "CHANGELOG.md", - &[ - Replacement::Append( - "## [Unreleased]".to_string(), - format!( - "\n## [{}] - {}", - bump.next, - chrono::Utc::now().format("%Y-%m-%d") + bump.bump_file("Cargo.toml", bump::replace_cargo)?; + bump.bump_file("README.md", bump::replace)?; + bump.bump_file("pkg/archlinux/projectr/PKGBUILD", bump::replace)?; + bump.bump_file("pkg/archlinux/projectr-bin/PKGBUILD", bump::replace)?; + bump.bump_file("pkg/archlinux/projectr-git/PKGBUILD", |buf, _| { + let stdout = std::process::Command::new("git") + .arg("describe") + .arg("--long") + .arg("--abbrev=7") + .output()? + .stdout; + + let pkgver = std::str::from_utf8(&stdout)? + .trim() + .trim_start_matches('v') + .replacen("-g", ".g", 1) + .replacen('-', "-r", 1) + .replace('-', "."); + + if let Some(from) = buf.lines().find(|l| l.starts_with("pkgver=")) { + Ok(buf.replace(from, &format!("pkgver={pkgver}"))) + } else { + Ok(buf) + } + })?; + + bump.bump_file("CHANGELOG.md", |buf, Bump { version: _, next }| { + let date = chrono::Utc::now().format("%Y-%m-%d"); + Ok(buf + .replace( + "## [Unreleased]", + &format!( + "## [Unreleased]\n\n\ + ## [{next}] - {date}" ), - ), - Replacement::Append( - "[Unreleased]: https://git.sr.ht/~tobyvin/projectr/log/HEAD".to_string(), - format!( - "[{0}]: https://git.sr.ht/~tobyvin/projectr/log/v{0}", - bump.next + ) + .replace( + "[Unreleased]: https://git.sr.ht/~tobyvin/projectr/log/HEAD", + &format!( + "[Unreleased]: https://git.sr.ht/~tobyvin/projectr/log/HEAD\n\ + [{next}]: https://git.sr.ht/~tobyvin/projectr/log/v{next}" ), - ), - ], - )?; + )) + })?; Ok(bump) } diff --git a/xtask/src/release/bump.rs b/xtask/src/release/bump.rs new file mode 100644 index 0000000..617cfd6 --- /dev/null +++ b/xtask/src/release/bump.rs @@ -0,0 +1,290 @@ +use std::{ + fmt::Display, + fs::File, + io::{Read, Write}, + path::Path, + process::Command, + str::FromStr, +}; + +use anyhow::Result; +use semver::Version; + +use crate::PKG_VER; + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum Level { + Major, + Minor, + #[default] + Patch, +} + +impl Level { + pub fn bump(&self, version: &Version) -> Version { + match self { + Self::Major => Version::new(version.major + 1, 0, 0), + Self::Minor => Version::new(version.major, version.minor + 1, 0), + Self::Patch => Version::new(version.major, version.minor, version.patch + 1), + } + } +} + +impl FromStr for Level { + type Err = anyhow::Error; + + fn from_str(s: &str) -> std::result::Result { + match s.to_lowercase().as_str() { + "major" => Ok(Level::Major), + "minor" => Ok(Level::Minor), + "patch" => Ok(Level::Patch), + s => Err(anyhow::anyhow!("Invalid bump level: {s}")), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Bump { + pub version: Version, + pub next: Version, +} + +impl Bump { + pub fn new(major: u64, minor: u64, patch: u64, level: Level) -> Self { + let version = Version::new(major, minor, patch); + let next = level.bump(&version); + Self { version, next } + } + + fn check_modified

(path: P) -> Result + where + P: AsRef, + { + Ok(Command::new("git") + .arg("diff-index") + .arg("--quiet") + .arg("HEAD") + .arg(path.as_ref()) + .output()? + .status + .success()) + } + + pub fn bump_file(&mut self, path: P, mutator: F) -> Result<()> + where + P: AsRef, + F: Fn(String, &Self) -> Result, + { + let path = path.as_ref(); + + anyhow::ensure!( + Self::check_modified(path)?, + "{} has uncommited changes. Aborting.", + path.display() + ); + + let file = File::open(path)?; + + self.bump(&file, &file, mutator)?; + + let git_added = Command::new("git").arg("add").arg(path).status()?; + + anyhow::ensure!(git_added.success(), "Failed to add bumped files to git"); + + Ok(()) + } + + fn bump(&self, mut reader: R, mut writer: W, mutator: F) -> Result<()> + where + R: Read, + W: Write, + F: Fn(String, &Self) -> Result, + { + let mut buf = String::new(); + reader.read_to_string(&mut buf)?; + + let buf = mutator(buf, self)?; + + writer.write_all(buf.as_bytes()).map_err(Into::into) + } +} + +impl Display for Bump { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} -> {}", self.version, self.next) + } +} + +impl Default for Bump { + fn default() -> Self { + let version = PKG_VER.parse().unwrap_or_else(|_| Version::new(0, 1, 0)); + Self { + next: Level::default().bump(&version), + version, + } + } +} + +impl From for Bump { + fn from(level: Level) -> Self { + let mut v = Self::default(); + v.next = level.bump(&v.version); + v + } +} + +/// Utility function for replacing version with next in a string. +pub fn replace(buf: String, Bump { version, next }: &Bump) -> Result { + Ok(buf.replace(&version.to_string(), &next.to_string())) +} + +/// Utility function for bumping the version in a Cargo.toml file. +pub fn replace_cargo(buf: String, Bump { version: _, next }: &Bump) -> Result { + let mut cargo_toml: toml_edit::Document = buf.parse()?; + + if cargo_toml["package"]["version"]["workspace"] + .as_bool() + .unwrap_or_default() + { + cargo_toml["workspace"]["package"]["version"] = toml_edit::value(next.to_string()); + } else if cargo_toml["package"]["version"].is_str() { + cargo_toml["package"]["version"] = toml_edit::value(next.to_string()); + } else { + anyhow::bail!("Failed to find version in Cargo.toml"); + }; + + Ok(cargo_toml.to_string()) +} + +#[cfg(test)] +mod test { + + use similar_asserts::SimpleDiff; + + use super::*; + + fn setup_test() -> Result<(), std::io::Error> { + let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("Failed to get parent directory.") + .to_path_buf(); + + std::env::set_current_dir(project_root) + } + + fn print_diff(reader: &[u8], writer: &[u8]) { + let left = std::str::from_utf8(reader).unwrap(); + let right = std::str::from_utf8(writer).unwrap(); + let diff = SimpleDiff::from_str(left, right, "old", "new"); + + assert_ne!(left, right); + println!("{diff}") + } + + #[test] + fn test_bump_cargo() { + setup_test().unwrap(); + + let bump = Bump::default(); + let file = "Cargo.toml"; + + let content = std::fs::read_to_string(file).unwrap(); + let reader = content.as_bytes(); + let mut writer = Vec::new(); + + bump.bump(reader, &mut writer, replace_cargo).unwrap(); + + println!("{file}: {bump}"); + print_diff(reader, &writer) + } + + #[test] + fn test_bump_changelog() { + setup_test().unwrap(); + + let bump = Bump::default(); + let file = "CHANGELOG.md"; + + let content = std::fs::read_to_string(file).unwrap(); + let reader = content.as_bytes(); + let mut writer = Vec::new(); + + bump.bump(reader, &mut writer, |buf, Bump { version: _, next }| { + let date = chrono::Utc::now().format("%Y-%m-%d"); + Ok(buf + .replace( + "## [Unreleased]", + &format!( + "## [Unreleased]\n\n\ + ## [{next}] - {date}" + ), + ) + .replace( + "[Unreleased]: https://git.sr.ht/~tobyvin/projectr/log/HEAD", + &format!( + "[Unreleased]: https://git.sr.ht/~tobyvin/projectr/log/HEAD\n\ + [{next}]: https://git.sr.ht/~tobyvin/projectr/log/v{next}" + ), + )) + }) + .unwrap(); + + println!("{file}: {bump}"); + print_diff(reader, &writer) + } + + #[test] + fn test_bump_readme() { + setup_test().unwrap(); + + let bump = Bump::default(); + let file = "README.md"; + + let content = std::fs::read_to_string(file); + let reader = content.as_deref().unwrap().as_bytes(); + let mut writer = Vec::new(); + + bump.bump(reader, &mut writer, replace).unwrap(); + + println!("{file}: {bump}"); + print_diff(reader, &writer) + } + + #[test] + fn test_bump_vsc_pkgbuild() { + setup_test().unwrap(); + + let bump = Bump::default(); + let file = "pkg/archlinux/projectr-git/PKGBUILD"; + + let content = std::fs::read_to_string(file).unwrap(); + let reader = content.as_bytes(); + let mut writer = Vec::new(); + + bump.bump(reader, &mut writer, |buf, _| { + let stdout = std::process::Command::new("git") + .arg("describe") + .arg("--long") + .arg("--abbrev=7") + .output()? + .stdout; + + let pkgver = std::str::from_utf8(&stdout)? + .trim() + .trim_start_matches('v') + .replacen("-g", ".g", 1) + .replacen('-', "-r", 1) + .replace('-', "."); + + if let Some(from) = buf.lines().find(|l| l.starts_with("pkgver=")) { + Ok(buf.replace(from, &format!("pkgver={pkgver}"))) + } else { + Ok(buf) + } + }) + .unwrap(); + + println!("{file}: {bump}"); + print_diff(reader, &writer) + } +} diff --git a/xtask/src/release/version.rs b/xtask/src/release/version.rs deleted file mode 100644 index 0c8cffd..0000000 --- a/xtask/src/release/version.rs +++ /dev/null @@ -1,302 +0,0 @@ -use std::{ - fmt::Display, - fs::File, - io::{Read, Write}, - path::Path, - process::Command, - str::FromStr, -}; - -use anyhow::Result; -use semver::Version; - -use crate::PKG_VER; - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] -pub enum Level { - Major, - Minor, - #[default] - Patch, -} - -impl Level { - pub fn bump(&self, version: &Version) -> Version { - match self { - Self::Major => Version::new(version.major + 1, 0, 0), - Self::Minor => Version::new(version.major, version.minor + 1, 0), - Self::Patch => Version::new(version.major, version.minor, version.patch + 1), - } - } -} - -impl FromStr for Level { - type Err = anyhow::Error; - - fn from_str(s: &str) -> std::result::Result { - match s.to_lowercase().as_str() { - "major" => Ok(Level::Major), - "minor" => Ok(Level::Minor), - "patch" => Ok(Level::Patch), - s => Err(anyhow::anyhow!("Invalid bump level: {s}")), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct Bump { - pub version: Version, - pub next: Version, -} - -impl Bump { - pub fn new(major: u64, minor: u64, patch: u64, level: Level) -> Self { - let version = Version::new(major, minor, patch); - let next = level.bump(&version); - Self { version, next } - } - - fn check_modified

(path: P) -> Result - where - P: AsRef, - { - Ok(Command::new("git") - .arg("diff-index") - .arg("--quiet") - .arg("HEAD") - .arg(path.as_ref()) - .output()? - .status - .success()) - } - - pub fn bump_file

(&mut self, path: P, replacements: &[Replacement]) -> Result<()> - where - P: AsRef, - { - let path = path.as_ref(); - - anyhow::ensure!( - Self::check_modified(path)?, - "{} has uncommited changes. Aborting.", - path.display() - ); - - let file = File::open(path)?; - - self.bump(&file, &file, replacements)?; - - let git_added = Command::new("git").arg("add").arg(path).status()?; - - anyhow::ensure!(git_added.success(), "Failed to add bumped files to git"); - - Ok(()) - } - - fn bump(&self, mut reader: R, mut writer: W, replacements: &[Replacement]) -> Result<()> - where - R: Read, - W: Write, - { - let mut buf = String::new(); - reader.read_to_string(&mut buf)?; - - let buf = replacements - .iter() - .try_fold(buf, |acc, r| r.replace(self, acc))?; - - writer.write_all(buf.as_bytes()).map_err(Into::into) - } -} - -impl Display for Bump { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{} -> {}", self.version, self.next) - } -} - -impl Default for Bump { - fn default() -> Self { - let version = PKG_VER.parse().unwrap_or_else(|_| Version::new(0, 1, 0)); - Self { - next: Level::default().bump(&version), - version, - } - } -} - -impl From for Bump { - fn from(level: Level) -> Self { - let mut v = Self::default(); - v.next = level.bump(&v.version); - v - } -} - -#[derive(Default)] -pub enum Replacement { - #[default] - Version, - CargoVersion, - FindLine(fn(&&str) -> bool, String), - Replace(String, String), - Append(String, String), -} - -impl Replacement { - pub fn replace(&self, bump: &Bump, buf: String) -> Result { - let buf = match self { - Replacement::Version => buf.replace(&bump.version.to_string(), &bump.next.to_string()), - Replacement::Replace(from, to) => buf.replace(from, to), - Replacement::Append(line, append) => buf.replace(line, &format!("{line}\n{append}")), - Replacement::FindLine(find, replace) => match buf.lines().find(find) { - Some(line) => buf.replace(line, replace), - None => buf, - }, - Replacement::CargoVersion => { - let mut cargo_toml: toml_edit::Document = buf.parse()?; - cargo_toml["workspace"]["package"]["version"] = - toml_edit::value(bump.next.to_string()); - cargo_toml.to_string() - } - }; - - Ok(buf) - } -} - -#[cfg(test)] -mod test { - - use similar_asserts::SimpleDiff; - - use super::*; - - fn setup_test() -> Result<(), std::io::Error> { - let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("Failed to get parent directory.") - .to_path_buf(); - - std::env::set_current_dir(project_root) - } - - fn print_diff(reader: &[u8], writer: &[u8]) { - let left = std::str::from_utf8(reader).unwrap(); - let right = std::str::from_utf8(writer).unwrap(); - let diff = SimpleDiff::from_str(left, right, "old", "new"); - - println!("{diff}") - } - - #[test] - fn test_bump_cargo() { - setup_test().unwrap(); - - let bump = Bump::default(); - let file = "Cargo.toml"; - - let content = std::fs::read_to_string(file).unwrap(); - let reader = content.as_bytes(); - let mut writer = Vec::new(); - - let replacements = &[Replacement::CargoVersion]; - - bump.bump(reader, &mut writer, replacements).unwrap(); - - println!("{file}: {bump}"); - print_diff(reader, &writer) - } - - #[test] - fn test_bump_changelog() { - setup_test().unwrap(); - - let bump = Bump::default(); - let file = "CHANGELOG.md"; - - let content = std::fs::read_to_string(file).unwrap(); - let reader = content.as_bytes(); - let mut writer = Vec::new(); - - let replacements = &[ - Replacement::Append( - "## [Unreleased]".to_string(), - format!( - "\n## [{}] - {}", - bump.next, - chrono::Utc::now().format("%Y-%m-%d") - ), - ), - Replacement::Append( - "[Unreleased]: https://git.sr.ht/~tobyvin/projectr/log/HEAD".to_string(), - format!( - "[{0}]: https://git.sr.ht/~tobyvin/projectr/log/v{0}", - bump.next - ), - ), - ]; - - bump.bump(reader, &mut writer, replacements).unwrap(); - - println!("{file}: {bump}"); - print_diff(reader, &writer) - } - - #[test] - fn test_bump_readme() { - setup_test().unwrap(); - - let bump = Bump::default(); - let file = "README.md"; - - let content = std::fs::read_to_string(file); - let reader = content.as_deref().unwrap().as_bytes(); - let mut writer = Vec::new(); - - let replacements = &[Replacement::Version]; - - bump.bump(reader, &mut writer, replacements).unwrap(); - - println!("{file}: {bump}"); - print_diff(reader, &writer) - } - - #[test] - fn test_bump_vsc_pkgbuild() { - setup_test().unwrap(); - - let bump = Bump::default(); - let file = "pkg/archlinux/projectr-git/PKGBUILD"; - - let content = std::fs::read_to_string(file).unwrap(); - let reader = content.as_bytes(); - let mut writer = Vec::new(); - - let stdout = std::process::Command::new("git") - .arg("describe") - .arg("--long") - .arg("--abbrev=7") - .output() - .unwrap() - .stdout; - - let pkgver = std::str::from_utf8(&stdout) - .unwrap() - .trim() - .trim_start_matches('v') - .replacen("-g", ".g", 1) - .replacen('-', "-r", 1) - .replace('-', "."); - - let replacements = &[Replacement::FindLine( - |l| l.starts_with("pkgver="), - format!("pkgver={pkgver}"), - )]; - - bump.bump(reader, &mut writer, replacements).unwrap(); - - println!("{file}: {bump}"); - print_diff(reader, &writer) - } -} -- cgit v1.2.3-70-g09d2