Commit 3636a55a authored by Jack Brown's avatar Jack Brown

feat(file): WIP supporting multiple version.txt files in subdirs

parent 13eecbf4
/target
**/*.rs.bk
*.rs.bk
*.swo
*.swp
.DS_Store
.vscode
*#
.#*
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -8,10 +8,12 @@ regex = ''
semver = ''
serde_json = ''
structopt = ''
tempfile = '3'
toml = ''
walkdir = '2'
[package]
authors = ['Jack Brown <[email protected]>']
edition = '2018'
name = 'verto'
version = '0.4.0'
version = '0.5.1'
.PHONY: build test release install readme doc clean version
.DEFAULT_GOAL := build
t ?=
build:
cargo build
test:
cargo test --nocapture
cargo test --no-fail-fast $(t) -- --nocapture
release:
release: test
cargo build --release
readme:
......@@ -16,11 +18,8 @@ readme:
doc:
cargo doc
install: release
for f in src/bin/*.rs; do \
sudo cp target/release/$$(basename $${f%.*}) \
/usr/local/bin/; \
done
install: test
cargo install --path . --force
clean:
rm -rf \
......
......@@ -6,7 +6,7 @@ use log::info;
use structopt::StructOpt;
#[derive(StructOpt)]
#[structopt(raw(setting = "structopt::clap::AppSettings::ColoredHelp"))]
#[structopt(setting = structopt::clap::AppSettings::ColoredHelp)]
#[structopt(rename_all = "kebab-case")]
struct Cli {
#[structopt(short, long)]
......@@ -65,7 +65,7 @@ fn main() {
let mut builder =
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_level));
builder.default_format_timestamp(false).init();
builder.format_timestamp(None).init();
// Determine the path to the target directory, defaulting to current directory
let path = &args.path.unwrap_or(PathBuf::from("."));
......@@ -93,16 +93,32 @@ fn main() {
}
// Initialize all the plugins
verto.initialize(&branch, &args.disable);
let current_version = verto.current_version(&args.read_from);
if let Err(e) = verto.initialize(&branch, &args.disable) {
println!("{}", e);
process::exit(1);
}
let current_version = match verto.current_version(&args.read_from) {
Ok(v) => v,
Err(e) => {
println!("{}", e);
process::exit(1);
}
};
// If the read arg is true, just print the version and exit
if args.read {
println!("{}", &current_version);
process::exit(0)
println!("{}", current_version);
process::exit(0);
}
let next_version = verto.next_version(&current_version, &args.increment_from);
let next_version = match verto.next_version(&current_version, &args.increment_from) {
Ok(v) => v,
Err(e) => {
println!("{}", e);
process::exit(1);
}
};
let files = verto.write(&next_version);
verto.commit(&next_version, &files);
......
use std::{fmt, io};
use git2;
use semver::SemVerError;
use walkdir;
#[derive(Debug)]
pub enum VertoError {
Version(String),
Io(io::Error),
Git(git2::Error),
Error(String),
}
impl fmt::Display for VertoError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
VertoError::Version(ref err) => write!(f, "Version error: {}", err),
VertoError::Io(ref err) => write!(f, "I/O error: {}", err),
VertoError::Git(ref err) => write!(f, "Git error: {}", err),
VertoError::Error(ref err) => write!(f, "Error: {}", err),
}
}
}
impl From<SemVerError> for VertoError {
fn from(_e: SemVerError) -> Self {
VertoError::Version("error parsing semver".to_string())
}
}
impl From<io::Error> for VertoError {
fn from(err: io::Error) -> Self {
VertoError::Io(err)
}
}
impl From<git2::Error> for VertoError {
fn from(err: git2::Error) -> Self {
VertoError::Git(err)
}
}
impl From<walkdir::Error> for VertoError {
fn from(err: walkdir::Error) -> Self {
VertoError::Error(err.to_string())
}
}
impl From<&str> for VertoError {
fn from(err: &str) -> Self {
VertoError::Error(String::from(err))
}
}
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::str;
use std::{
collections::HashMap,
path::{Path, PathBuf},
result, str,
};
use git2::Error;
use log::{debug, info};
use semver::Version;
mod error;
pub use error::VertoError as Error;
mod plugins {
// ADDING A PLUGIN: Add the module to this list
pub mod file;
......@@ -14,20 +18,25 @@ mod plugins {
pub mod rust;
}
// Pluggable must be implemented for all plugins, and contains basic functionality for working with
// the plugin, lifecycle hooks, and providing a name in order to enable/disable the plugin.
pub type Result<T> = result::Result<T, Error>;
// Pluggable must be implemented for all plugins, and contains basic
// functionality for working with the plugin, lifecycle hooks, and providing a
// name in order to enable/disable the plugin.
pub trait Plugable {
fn name(&self) -> String;
// check is run first to determine whether a give plugin should be enabled for use
fn check(&self) -> bool;
// check is run first to determine whether a give plugin should be enabled
// for use
fn check(&self) -> Result<bool>;
fn prefix(&self) -> &str;
// TODO: Figure out how to make this return itself in the builder pattern
fn enable(&mut self);
fn disable(&mut self);
fn enabled(&self) -> bool;
// Run any required initialization code
fn init(&mut self, _branch: &Option<String>) -> Result<(), Error> {
fn init(&mut self, _branch: &Option<String>) -> Result<()> {
Ok(())
}
......@@ -36,29 +45,28 @@ pub trait Plugable {
format!("{}{}", self.prefix(), version)
}
// Strip the prefix from the semver string
/// Strip the prefix from the semver string.
fn remove_prefix(&self, version: &str) -> String {
version.replace(&self.prefix(), "")
}
// Calculate what the current version is
// TODO: Return a Result here so we can handle failures
fn current_version(&self) -> Option<Version> {
None
/// Calculate what the current version is.
fn current_version(&self) -> Result<Option<Version>> {
Ok(None)
}
// Calculate what the next version should be
fn next_version(&self, _: &Version) -> Option<Version> {
None
/// Calculate what the next version should be.
fn next_version(&self, _: &Version) -> Result<Option<Version>> {
Ok(None)
}
// Write the version to file(s)
/// Write the version to file(s).
fn write(&self, _version: &str) -> Vec<PathBuf> {
vec![]
}
// Called after all plugins have written
fn done(&self, _version: &str, _changed_files: &Vec<&Path>) -> Result<(), Error> {
fn done(&self, _version: &str, _changed_files: &Vec<&Path>) -> Result<()> {
Ok(())
}
}
......@@ -99,7 +107,11 @@ impl Verto {
.collect::<Vec<String>>()
}
pub fn initialize(&mut self, branch: &Option<String>, disabled_plugins: &Vec<String>) {
pub fn initialize(
&mut self,
branch: &Option<String>,
disabled_plugins: &Vec<String>,
) -> Result<()> {
debug!("plugins disabled: {:?}", disabled_plugins);
// Go through all the available plugins, and filter them based on
......@@ -109,7 +121,7 @@ impl Verto {
for plugin in self.plugins.iter_mut() {
// If the plugin has not detected that it's relevant for this project, disable it and
// continue
if !plugin.check() {
if !plugin.check()? {
debug!("==> {} not detected; disabling", &plugin.name());
plugin.disable();
continue;
......@@ -141,6 +153,7 @@ impl Verto {
"==> plugins enabled: {}",
self.plugins
.iter()
.filter(|p| p.enabled())
.map(|p| p.name())
.collect::<Vec<String>>()
.join(", ")
......@@ -152,11 +165,13 @@ impl Verto {
continue;
}
plugin.init(branch).unwrap();
plugin.init(branch)?;
}
Ok(())
}
pub fn current_version(&self, plugin_name: &Option<String>) -> Version {
pub fn current_version(&self, plugin_name: &Option<String>) -> Result<Version> {
if let Some(name) = plugin_name {
info!("==> only incrementing version using plugin: {}", &name);
}
......@@ -174,30 +189,40 @@ impl Verto {
}
}
if let Some(v) = plugin.current_version() {
if let Some(v) = plugin.current_version()? {
info!("{} at version {}", plugin.name(), v);
current_versions.insert(plugin.name(), v);
}
}
debug!("found current versions: {:?}", &current_versions);
debug!("found current versions: {:#?}", &current_versions);
// Now that we have a set of current_versions from the plugins, confirm that they're all indicating the
// same version
let mut current_versions: Vec<&Version> = current_versions.values().collect();
if current_versions.len() < 1 {
return Version::parse("0.0.0").expect("unable to parse default version");
return Ok(Version::parse("0.0.0").expect("unable to parse default version"));
}
current_versions.sort();
current_versions.dedup();
if current_versions.len() > 1 {
panic!("different current versions detected by different plugins; check that all tags have been pushed, and that you've pulled the latest tags. If they still don't match, make sure that you ran a build to update any lockfiles if you bumped the version by hand somewhere");
return Err(Error::from(
"different current versions detected by different plugins;
check that all tags have been pushed, and that you've pulled the latest tags.
If they still don't match, make sure that you ran a build to update any
lockfiles if you bumped the version by hand somewhere",
));
}
let current_version = current_versions[0];
info!("==> current version: {}", &current_version.to_string());
current_version.clone()
Ok(current_version.clone())
}
pub fn next_version(&self, current_version: &Version, plugin_name: &Option<String>) -> Version {
pub fn next_version(
&self,
current_version: &Version,
plugin_name: &Option<String>,
) -> Result<Version> {
if let Some(name) = plugin_name {
info!("==> only using version from plugin: {}", &name);
}
......@@ -215,7 +240,7 @@ impl Verto {
}
}
if let Some(v) = plugin.next_version(current_version) {
if let Some(v) = plugin.next_version(current_version)? {
next_versions.insert(plugin.name(), v);
}
}
......@@ -225,17 +250,19 @@ impl Verto {
// same version
let mut next_versions: Vec<&Version> = next_versions.values().collect();
if next_versions.len() < 1 {
panic!("no enabled plugin returned a next version");
return Err(Error::from("no enabled plugin returned a next version"));
}
next_versions.sort();
next_versions.dedup();
if next_versions.len() > 1 {
panic!("different next version returned by different plugins");
return Err(Error::from(
"different next version returned by different plugins",
));
}
let next_version = next_versions[0];
info!("==> next version: {}", &next_version.to_string());
next_version.clone()
Ok(next_version.clone())
}
pub fn write(&mut self, next_version: &Version) -> Vec<PathBuf> {
......
This diff is collapsed.
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::str;
use std::{
fmt::Debug,
path::{Path, PathBuf},
str,
};
use crate::{Error, Result};
use chrono::Local;
use git2::{Commit, Error, Oid, Repository, RepositoryState, Signature, Time};
use git2::{Commit, Oid, Repository, RepositoryState, Signature, Time};
use log::debug;
use regex::Regex;
use semver::Version;
......@@ -29,7 +32,7 @@ impl Plugin {
plug
}
fn versions(&self) -> Result<Vec<Version>, Error> {
fn versions(&self) -> Result<Vec<Version>> {
use crate::Plugable;
let mut versions = Vec::new();
......@@ -79,24 +82,25 @@ impl Plugin {
head_commit
}
fn branch(&self) -> Result<String, Error> {
Ok(str::replace(
&String::from(self.repo.as_ref().unwrap().head().unwrap().name().unwrap()),
"refs/heads/",
"",
))
fn branch(&self) -> Result<String> {
let repo = match self.repo.as_ref() {
Some(r) => r,
None => return Err(Error::from("unable to get repo reference")),
};
match repo.head()?.name() {
Some(n) => Ok(str::replace(&String::from(n), "refs/heads/", "")),
None => Err(Error::from("unable to get repo name")),
}
}
fn commit(&self, parent: &Commit, version: &str, paths: &Vec<&Path>) -> Result<Oid, Error> {
fn commit(&self, parent: &Commit, version: &str, paths: &Vec<&Path>) -> Result<Oid> {
// Generate a signature to use
let sig = self.signature();
// Add version.txt to staged changes
let mut index = self.repo.as_ref().unwrap().index()?;
debug!("index length: {}\ncontents:", &index.len());
for e in index.iter() {
debug!("{:?}", str::from_utf8(&e.path));
}
for path in paths {
// Get the relative path to add
......@@ -107,27 +111,26 @@ impl Plugin {
}
debug("index length", &index.len());
for e in index.iter() {
unsafe {
debug!("{:?}", str::from_utf8_unchecked(&e.path));
}
}
index.write()?;
let oid = index.write_tree()?;
debug("oid", &oid);
self.repo.as_ref().unwrap().commit(
Some("HEAD"),
&sig,
&sig,
&format!("VERSION {}", version),
&self
.repo
.as_ref()
.unwrap()
.find_tree(oid)
.expect("unable to find tree for index"),
&[parent],
)
match self.repo.as_ref() {
Some(repo) => Ok(repo.commit(
Some("HEAD"),
&sig,
&sig,
&format!("VERSION {}", version),
&self
.repo
.as_ref()
.unwrap()
.find_tree(oid)
.expect("unable to find tree for index"),
&[parent],
)?),
None => Err(Error::Error("unable to get repo reference".to_string())),
}
}
fn signature(&self) -> Signature {
......@@ -149,8 +152,8 @@ impl crate::Plugable for Plugin {
&self.prefix
}
fn check(&self) -> bool {
Repository::open(&self.path).is_ok()
fn check(&self) -> Result<bool> {
Ok(Repository::open(&self.path).is_ok())
}
fn enable(&mut self) {
......@@ -171,57 +174,57 @@ impl crate::Plugable for Plugin {
false
}
fn init(&mut self, branch: &Option<String>) -> Result<(), Error> {
let repo = Repository::open(&self.path)?;
fn init(&mut self, branch: &Option<String>) -> Result<()> {
self.repo = Some(Repository::open(&self.path)?);
match repo.state() {
match self.repo.as_ref().unwrap().state() {
RepositoryState::Clean => {
self.repo = Some(repo);
if let Some(b) = branch {
if &self.branch().unwrap() != b {
return Err(Error::from_str("repo is not on the allowed branch!"));
if &self.branch().expect("error getting current branch") != b {
return Err(Error::Error(
"repo is not on the allowed branch!".to_string(),
));
}
}
}
state => {
return Err(Error::Error(format!(
"repository not clean: state: {:?}",
state
)))
}
}
Ok(())
// Make sure this isn't a version-bump commit
// TODO: Update this to actually detect correctly
if let Some(name) = self.head().author().name() {
if name == "verto" {
return Err(Error::Error(
"HEAD points to a branch whose last commit was a version bump".to_string(),
));
}
state => Err(Error::from_str(&format!(
"repository not clean: state: {:?}",
state
))),
}
Ok(())
}
fn current_version(&self) -> Option<Version> {
fn current_version(&self) -> Result<Option<Version>> {
let mut versions = self.versions().unwrap();
// Grab the latest version if we can find one
match versions.len() {
n if n > 0 => Some(versions.pop().expect("unable to find a version")),
_ => None,
n if n > 0 => Ok(Some(versions.pop().expect("unable to find a version"))),
_ => Ok(None),
}
}
fn next_version(&self, v: &Version) -> Option<Version> {
fn next_version(&self, v: &Version) -> Result<Option<Version>> {
// Get the commit that the latest version points to
let latest_version_commit = self
.repo
.as_ref()
.expect("unable to get ref for repo")
.find_reference(&format!("refs/tags/{}", self.add_prefix(v)));
// Make sure this isn't a version-bump commit
// TODO: Update this to actually detect correctly
if self
.head()
.author()
.name()
.expect("unable to get author name")
== "verto"
{
panic!("HEAD points to a branch whose last commit was a version bump")
}
.find_reference(&format!("refs/tags/{}", self.add_prefix(v)))?;
let mut walk = self
.repo
......@@ -232,13 +235,12 @@ impl crate::Plugable for Plugin {
walk.push_head().expect("error pushing HEAD onto revwalk");
if let Ok(c) = latest_version_commit {
walk.hide(
c.target()
.expect("unable to get target for latest version commit"),
)
.expect("error pushing HEAD onto revwalk");
}
walk.hide(
latest_version_commit
.target()
.expect("unable to get target for latest version commit"),
)
.expect("error pushing HEAD onto revwalk");
let mut commits = Vec::new();
for n in walk.into_iter() {
......@@ -249,10 +251,10 @@ impl crate::Plugable for Plugin {
let new_version = calculate_new_version(v, &commits).unwrap();
Some(new_version)
Ok(Some(new_version))
}
fn done(&self, version: &str, files: &Vec<&Path>) -> Result<(), Error> {
fn done(&self, version: &str, files: &Vec<&Path>) -> Result<()> {
let new_commit_oid = self
.commit(&self.head(), &self.add_prefix(&version), files)
.expect("error committing version changes");
......@@ -283,11 +285,11 @@ impl crate::Plugable for Plugin {
}
}
fn debug<T: Debug>(s: &str, input: &T) {
fn debug<'a, T: Debug>(s: &str, input: &T) {
debug!("{}: {:?}", s, input)
}
fn calculate_new_version(version: &Version, commits: &Vec<Commit>) -> Result<Version, Error> {
fn calculate_new_version(version: &Version, commits: &Vec<Commit>) -> Result<Version> {
// build a regex to extract version keywords
let re = Regex::new(r"(feat|fix)(\(.*\))?:").unwrap();
......@@ -316,3 +318,24 @@ fn calculate_new_version(version: &Version, commits: &Vec<Commit>) -> Result<Ver
Ok(new_version)
}
#[cfg(test)]
mod tests {
/* TODO: Make these tests work by creating dedicated git repos in temp dirs
use super::*;
use crate::Plugable;