From c6e8c15b39781ce48cd3f0d829ab5c6574ccd01b Mon Sep 17 00:00:00 2001 From: Toby Vincent Date: Sat, 3 Jun 2023 21:34:21 -0500 Subject: build: add xtask release command --- xtask/src/release/version.rs | 302 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 xtask/src/release/version.rs (limited to 'xtask/src/release') diff --git a/xtask/src/release/version.rs b/xtask/src/release/version.rs new file mode 100644 index 0000000..0c8cffd --- /dev/null +++ b/xtask/src/release/version.rs @@ -0,0 +1,302 @@ +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