diff options
-rw-r--r-- | bumper.md | 15 | ||||
-rw-r--r-- | src/bin/bumper.rs | 21 | ||||
-rw-r--r-- | src/debian.rs | 18 | ||||
-rw-r--r-- | src/errors.rs | 15 | ||||
-rw-r--r-- | src/git.rs | 4 | ||||
-rw-r--r-- | src/lib.rs | 1 | ||||
-rw-r--r-- | src/project.rs | 8 | ||||
-rw-r--r-- | src/python.rs | 22 | ||||
-rw-r--r-- | src/rust.rs | 4 | ||||
-rw-r--r-- | src/tag.rs | 168 | ||||
-rw-r--r-- | subplot/bumper.py | 4 | ||||
-rw-r--r-- | subplot/bumper.yaml | 3 |
12 files changed, 273 insertions, 10 deletions
@@ -146,15 +146,24 @@ release for a Python project. ~~~scenario given an installed Bumper -given file foo/setup.py from empty +given file foo/setup.py from setup.py +given file foo/setup.py is executable given file foo/lib/version.py from empty given all files in foo are committed to git -when I run, in foo, bumper 105.12765.42 +when I run, in foo, bumper --tag=foo-%v 105.12765.42 then all changes in foo are committed -then in foo, git tag v105.12765.42 is a signed tag +then in foo, git tag foo-105.12765.42 is a signed tag then file foo/lib/version.py matches regex /__version__\s*=\s*"105\.12765\.42"/ ~~~ +~~~{#setup.py .file .python} +#!/usr/bin/env python3 +from distutils.core import setup +setup( + name="vmdb2", +) +~~~ + --- title: bumper – set version number for a project diff --git a/src/bin/bumper.rs b/src/bin/bumper.rs index 59443e5..9fbf898 100644 --- a/src/bin/bumper.rs +++ b/src/bin/bumper.rs @@ -1,6 +1,7 @@ use bumper::errors::BumperError; use bumper::git; use bumper::project::ProjectKind; +use bumper::tag::Tag; use log::{debug, error}; use std::path::{Path, PathBuf}; use std::process::exit; @@ -20,14 +21,25 @@ fn bumper() -> Result<(), BumperError> { let cwd = abspath(".")?; println!("Setting version for project in {}", cwd.display()); - for mut kind in ProjectKind::detect(&cwd)? { + + let mut kinds = ProjectKind::detect(&cwd)?; + if kinds.is_empty() { + return Err(BumperError::UnknownProjectKind(cwd)); + } + let name = kinds[0].name()?; + + for kind in kinds.iter_mut() { let version = kind.set_version(&opt.version)?; println!("{} project set to {}", kind.desc(), version); } - let msg = format!("Set version to {}", opt.version); + let msg = format!("Release version {}", opt.version); git::commit(".", &msg)?; - git::tag(".", &opt.version)?; + + let tag = Tag::new(&opt.tag)?; + let tag = tag.apply(&name, &opt.version); + println!("release tag: {}", tag); + git::tag(".", &tag, &msg)?; debug!("Bumper ends OK"); Ok(()) } @@ -41,6 +53,9 @@ fn abspath<P: AsRef<Path>>(path: P) -> Result<PathBuf, BumperError> { #[derive(Debug, StructOpt)] struct Opt { + #[structopt(long, default_value = "v%v")] + tag: String, + #[structopt(help = "version number of new release")] version: String, } diff --git a/src/debian.rs b/src/debian.rs index 3f9e01b..15b87ed 100644 --- a/src/debian.rs +++ b/src/debian.rs @@ -19,6 +19,24 @@ impl Debian { Err(BumperError::UnknownProjectKind(dirname.to_path_buf())) } + pub fn name(&self) -> Result<String, BumperError> { + let output = Command::new("dpkg-parsechangelog") + .arg("-SSource") + .current_dir(&self.dirname) + .output() + .map_err(|err| BumperError::ParseChangelogInvoke(self.dirname.to_path_buf(), err))?; + if output.status.success() { + let name = String::from_utf8_lossy(&output.stdout).into_owned(); + Ok(name.trim_end().to_string()) + } else { + let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); + Err(BumperError::ParseChangelog( + self.dirname.to_path_buf(), + stderr, + )) + } + } + pub fn set_version(&mut self, version: &str) -> Result<String, BumperError> { let version = format!("{}-1", version); self.dch(&["-v", &version, ""])?; diff --git a/src/errors.rs b/src/errors.rs index 97ed8d8..a296afd 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -2,6 +2,9 @@ use std::path::PathBuf; #[derive(Debug, thiserror::Error)] pub enum BumperError { + #[error("Tag pattern is not ASCII: {0}")] + TagPatternNotAscii(String), + #[error("Can't figure out what kind of project: {0}")] UnknownProjectKind(PathBuf), @@ -44,6 +47,18 @@ pub enum BumperError { #[error("dch failed in {0}: {1}")] Dch(PathBuf, String), + #[error("Failed to run dpkg-parsechangelog in {0}: {1}")] + ParseChangelogInvoke(PathBuf, #[source] std::io::Error), + + #[error("dpkg-parsechangelog failed in {0}: {1}")] + ParseChangelog(PathBuf, String), + + #[error("Failed to run setup.py in {0}: {1}")] + Setupnvoke(PathBuf, #[source] std::io::Error), + + #[error("setup.py failed in {0}: {1}")] + Setup(PathBuf, String), + #[error("Failed to run cargo in {0}: {1}")] CargoInvoke(PathBuf, #[source] std::io::Error), @@ -3,9 +3,7 @@ use log::debug; use std::path::Path; use std::process::Command; -pub fn tag<P: AsRef<Path>>(dirname: P, version: &str) -> Result<(), BumperError> { - let msg = format!("release version {}", version); - let tag_name = format!("v{}", version); +pub fn tag<P: AsRef<Path>>(dirname: P, tag_name: &str, msg: &str) -> Result<(), BumperError> { git(dirname.as_ref(), &["tag", "-am", &msg, &tag_name])?; Ok(()) } @@ -4,3 +4,4 @@ pub mod git; pub mod project; pub mod python; pub mod rust; +pub mod tag; diff --git a/src/project.rs b/src/project.rs index 655fb23..9941ef7 100644 --- a/src/project.rs +++ b/src/project.rs @@ -48,6 +48,14 @@ impl ProjectKind { } } + pub fn name(&mut self) -> Result<String, BumperError> { + match self { + Self::Debian(x) => x.name(), + Self::Python(x) => x.name(), + Self::Rust(x) => x.name(), + } + } + pub fn set_version(&mut self, version: &str) -> Result<String, BumperError> { Ok(match self { Self::Rust(ref mut rust) => rust.set_version(version)?, diff --git a/src/python.rs b/src/python.rs index 4cc043e..28c0aec 100644 --- a/src/python.rs +++ b/src/python.rs @@ -2,8 +2,10 @@ use crate::errors::BumperError; use glob::glob; use log::{debug, info}; use std::path::{Path, PathBuf}; +use std::process::Command; pub struct Python { + dirname: PathBuf, version_pys: Vec<PathBuf>, } @@ -17,13 +19,31 @@ impl Python { debug!("no version.py files in {}", dirname.display()); Err(BumperError::NoVersionPy(dirname.to_path_buf())) } else { - Ok(Self { version_pys: files }) + Ok(Self { + dirname: dirname.to_path_buf(), + version_pys: files, + }) } } else { Err(BumperError::UnknownProjectKind(dirname.to_path_buf())) } } + pub fn name(&mut self) -> Result<String, BumperError> { + let output = Command::new("./setup.py") + .arg("--name") + .current_dir(&self.dirname) + .output() + .map_err(|err| BumperError::Setupnvoke(self.dirname.to_path_buf(), err))?; + if output.status.success() { + let name = String::from_utf8_lossy(&output.stdout).into_owned(); + Ok(name.trim_end().to_string()) + } else { + let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); + Err(BumperError::Setup(self.dirname.to_path_buf(), stderr)) + } + } + pub fn set_version(&mut self, version: &str) -> Result<String, BumperError> { for filename in self.version_pys.iter() { info!("writing Python version to {}", filename.display()); diff --git a/src/rust.rs b/src/rust.rs index 0ffe8c0..2dbe1f7 100644 --- a/src/rust.rs +++ b/src/rust.rs @@ -27,6 +27,10 @@ impl Rust { Err(BumperError::UnknownProjectKind(dirname.to_path_buf())) } + pub fn name(&mut self) -> Result<String, BumperError> { + self.cargo_toml.name() + } + pub fn set_version(&mut self, version: &str) -> Result<String, BumperError> { debug!("parsing Cargo.toml from {}", self.dirname.display()); // debug!("Cargo.toml:\n{:#?}", self.cargo_toml); diff --git a/src/tag.rs b/src/tag.rs new file mode 100644 index 0000000..5a6b4df --- /dev/null +++ b/src/tag.rs @@ -0,0 +1,168 @@ +use crate::errors::BumperError; + +/// Name git tags after a pattern. +/// +/// A `Tag` is created from a text template, which can embed the +/// following for values to be filled in later: +/// +/// - `%%` – a single percent character +/// - `%n` – the name of the project +/// - `%v` – the release version number +/// +/// The template is parsed by `Tag::new`, which can fail, and the +/// values for project name and release version are filled in by +/// `Tag::apply`. +pub struct Tag { + patterns: Vec<Pattern>, +} + +impl Tag { + pub fn new(template: &str) -> Result<Self, BumperError> { + Ok(Self { + patterns: parse_template(template)?, + }) + } + + pub fn apply(&self, name: &str, version: &str) -> String { + let mut result = String::new(); + for p in self.patterns.iter() { + match p { + Pattern::Fixed(s) => result.push_str(s), + Pattern::Name => result.push_str(name), + Pattern::Version => result.push_str(version), + } + } + result + } +} + +#[derive(Debug)] +enum Pattern { + Fixed(String), + Name, + Version, +} + +impl PartialEq for &Pattern { + fn eq(&self, other: &Self) -> bool { + match self { + Pattern::Fixed(s) => match other { + Pattern::Fixed(t) => s == t, + _ => false, + }, + Pattern::Name => matches!(other, Pattern::Name), + Pattern::Version => matches!(other, Pattern::Version), + } + } +} + +fn parse_template(t: &str) -> Result<Vec<Pattern>, BumperError> { + if !t.is_ascii() { + return Err(BumperError::TagPatternNotAscii(t.to_string())); + } + + let mut result = vec![]; + let mut t = t.to_string(); + + while !t.is_empty() { + let p = if t.starts_with("%%") { + t.drain(..2); + Pattern::Fixed("%".to_string()) + } else if t.starts_with("%v") { + t.drain(..2); + Pattern::Version + } else if t.starts_with("%n") { + t.drain(..2); + Pattern::Name + } else { + let c = t.get(..1).unwrap().to_string(); + t.drain(..1); + Pattern::Fixed(c) + }; + + // Combine fixed string with previous fixed string. + if let Pattern::Fixed(b) = &p { + if let Some(last) = result.pop() { + // There was a previous pattern. Is it a fixed string? + if let Pattern::Fixed(a) = last { + let mut ab = a.clone(); + ab.push_str(&b); + result.push(Pattern::Fixed(ab)) + } else { + result.push(last); + result.push(p); + } + } else { + // No previous pattern. + result.push(p); + } + } else { + result.push(p); + }; + } + + Ok(result) +} + +#[cfg(test)] +mod test { + use super::{parse_template, Pattern, Tag}; + + fn vecs_eq(this: &[Pattern], that: &[Pattern]) -> bool { + println!(); + println!("this: {:?}", this); + println!("that: {:?}", that); + this.iter().eq(that.iter()) + } + + #[test] + fn empty_template() { + assert!(vecs_eq(&parse_template("").unwrap(), &[])); + } + + #[test] + fn fixed_string() { + assert!(vecs_eq( + &parse_template("foo").unwrap(), + &[Pattern::Fixed("foo".to_string())], + )); + } + + #[test] + fn percent() { + assert!(vecs_eq( + &parse_template("%%").unwrap(), + &[Pattern::Fixed("%".to_string())] + )); + } + + #[test] + fn version() { + assert!(vecs_eq(&parse_template("%v").unwrap(), &[Pattern::Version])); + } + + #[test] + fn name() { + assert!(vecs_eq(&parse_template("%n").unwrap(), &[Pattern::Name])); + } + + #[test] + fn many_parts() { + assert!(vecs_eq( + &parse_template("this-is-a-%n-%v-RELEASE").unwrap(), + &[ + Pattern::Fixed("this-is-a-".to_string()), + Pattern::Name, + Pattern::Fixed("-".to_string()), + Pattern::Version, + Pattern::Fixed("-RELEASE".to_string()) + ] + )); + } + + #[test] + fn apply() { + let tag = Tag::new("%n-%v").unwrap(); + assert_eq!(tag.apply("foo", "1.2.3"), "foo-1.2.3"); + } +} diff --git a/subplot/bumper.py b/subplot/bumper.py index 3edad5e..3c2f8fd 100644 --- a/subplot/bumper.py +++ b/subplot/bumper.py @@ -11,6 +11,10 @@ def install_bumper(ctx): runcmd_prepend_to_path(ctx, dirname=os.path.join(srcdir, "target", "debug")) +def chmod_exec(ctx, filename=None): + os.chmod(filename, 0o755) + + def git_init_and_commit_everything(ctx, dirname=None): runcmd_run = globals()["runcmd_run"] runcmd_exit_code_is_zero = globals()["runcmd_exit_code_is_zero"] diff --git a/subplot/bumper.yaml b/subplot/bumper.yaml index 6109c59..83aae28 100644 --- a/subplot/bumper.yaml +++ b/subplot/bumper.yaml @@ -1,6 +1,9 @@ - given: "an installed Bumper" function: install_bumper +- given: "file {filename} is executable" + function: chmod_exec + - given: "all files in {dirname} are committed to git" function: git_init_and_commit_everything |