summaryrefslogtreecommitdiffstats
path: root/xtask
diff options
context:
space:
mode:
Diffstat (limited to 'xtask')
-rw-r--r--xtask/Cargo.toml10
-rw-r--r--xtask/src/lib.rs20
-rw-r--r--xtask/src/main.rs3
-rw-r--r--xtask/src/release.rs174
-rw-r--r--xtask/src/release/version.rs302
5 files changed, 509 insertions, 0 deletions
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
index 5040f0a..1694407 100644
--- a/xtask/Cargo.toml
+++ b/xtask/Cargo.toml
@@ -13,3 +13,13 @@ anyhow = { workspace = true }
clap = { workspace = true }
tar = "0.4.38"
flate2 = "1.0.26"
+semver = "1.0.17"
+toml_edit = { version = "0.19.10", features = ["serde"] }
+
+[dependencies.chrono]
+version = "0.4.26"
+default-features = false
+features = ["std", "clock"]
+
+[dev-dependencies]
+similar-asserts = "1.4.2"
diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs
index f01fe7b..26dcbd1 100644
--- a/xtask/src/lib.rs
+++ b/xtask/src/lib.rs
@@ -8,3 +8,23 @@ const PKG_INCLUDE: &[&str] = &[
];
pub mod dist;
+pub mod release;
+
+/// Parse version from git describe output.
+pub fn git_version() -> anyhow::Result<semver::Version> {
+ let stdout = std::process::Command::new("git")
+ .arg("describe")
+ .arg("--long")
+ .arg("--abbrev=7")
+ .output()?
+ .stdout;
+
+ std::str::from_utf8(&stdout)?
+ .trim()
+ .trim_start_matches('v')
+ .replacen("-g", ".g", 1)
+ .replacen('-', "-r", 1)
+ .parse()
+ .map_err(Into::into)
+}
+
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 51d5491..7454bf0 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -26,6 +26,7 @@ fn main() -> Result<()> {
let out_dir = dist::find_out_dir(&cli.profile)?;
println!("{}", out_dir.display());
}
+ Commands::Release(release) => release.run()?,
};
Ok(())
@@ -57,6 +58,8 @@ enum Commands {
#[arg(short, long)]
tag: Option<String>,
},
+
+ Release(release::Release),
}
fn get_package_dir() -> PathBuf {
diff --git a/xtask/src/release.rs b/xtask/src/release.rs
new file mode 100644
index 0000000..8bf4444
--- /dev/null
+++ b/xtask/src/release.rs
@@ -0,0 +1,174 @@
+use std::process::{Command, Stdio};
+
+use anyhow::Result;
+use clap::{Args, Subcommand};
+use semver::Version;
+
+use self::version::{Bump, Level, Replacement};
+
+mod version;
+
+#[derive(Debug, Clone, Args)]
+pub struct Release {
+ #[command(subcommand)]
+ step: Option<Step>,
+
+ /// Level of version bump version.
+ #[arg(global = true, required = false)]
+ level: version::Level,
+
+ /// Options passed to git commit.
+ #[arg(global = true, last = true)]
+ git_commit_args: Vec<String>,
+}
+
+impl Release {
+ pub fn run(self) -> Result<()> {
+ match self.step {
+ Some(step) => step.run(),
+ None => {
+ let bump = Step::bump(self.level)?;
+
+ println!("Bumped version: {bump}");
+
+ Ok(())
+ }
+ }
+ }
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum Step {
+ /// Bump version in package files and commit changes.
+ Bump {
+ #[arg(from_global)]
+ level: version::Level,
+ },
+
+ /// Make a release commit.
+ Commit {
+ #[arg(from_global)]
+ git_commit_args: Vec<String>,
+ },
+
+ /// Create git tag for release.
+ Tag {
+ #[arg(from_global)]
+ level: version::Level,
+ },
+}
+
+impl Step {
+ pub fn run(self) -> Result<()> {
+ match self {
+ Step::Bump { level } => {
+ let bump = Self::bump(level)?;
+ println!("Bumped version: {bump}");
+ }
+ Step::Commit { git_commit_args } => Self::commit(git_commit_args)?,
+ Step::Tag { level } => {
+ let stdout = Command::new("git")
+ .arg("describe")
+ .arg("--abbrev=0")
+ .output()?
+ .stdout;
+
+ let prev = std::str::from_utf8(&stdout)?.parse()?;
+ let next = level.bump(&prev);
+ Self::tag(prev, next)?;
+ }
+ };
+
+ Ok(())
+ }
+
+ pub fn bump(level: Level) -> Result<Bump> {
+ 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")
+ ),
+ ),
+ 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
+ ),
+ ),
+ ],
+ )?;
+
+ Ok(bump)
+ }
+
+ pub fn commit(git_commit_args: Vec<String>) -> Result<()> {
+ let git_commit = Command::new("git")
+ .arg("commit")
+ .args(git_commit_args)
+ .status()?;
+
+ anyhow::ensure!(git_commit.success(), "Failed to commit changes");
+
+ Ok(())
+ }
+
+ pub fn tag(from: Version, to: Version) -> Result<String> {
+ let tag_name = format!("v{}", to);
+
+ let shortlog_child = Command::new("git")
+ .arg("shortlog")
+ .arg(format!("v{}..HEAD", from))
+ .arg("--abbrev=7")
+ .stdout(Stdio::piped())
+ .spawn()?;
+
+ let git_commit = Command::new("git")
+ .arg("tag")
+ .arg("-s")
+ .arg(&tag_name)
+ .arg("--file")
+ .arg("-")
+ .stdin(Stdio::from(shortlog_child.stdout.unwrap())) // Pipe through.
+ .status()?;
+
+ anyhow::ensure!(git_commit.success(), "Failed to commit changes");
+
+ Ok(tag_name)
+ }
+}
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<Self, Self::Err> {
+ 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<P>(path: P) -> Result<bool>
+ where
+ P: AsRef<Path>,
+ {
+ Ok(Command::new("git")
+ .arg("diff-index")
+ .arg("--quiet")
+ .arg("HEAD")
+ .arg(path.as_ref())
+ .output()?
+ .status
+ .success())
+ }
+
+ pub fn bump_file<P>(&mut self, path: P, replacements: &[Replacement]) -> Result<()>
+ where
+ P: AsRef<Path>,
+ {
+ 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<R, W>(&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<Level> 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<String> {
+ 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)
+ }
+}