Skip to content

Commit

Permalink
feat: pre-commit bump hooks
Browse files Browse the repository at this point in the history
Support user-defined hooks in cog.toml, running after version bump and before the commit. Hooks can accept '%version' parameter with the version bump result.
  • Loading branch information
mersinvald committed Oct 9, 2020
1 parent 5de1900 commit c11147d
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ serde = "^1"
tempdir = "^0"
semver = "0.10.0"
moins = "0.4.0"
shell-words = "^1"
lazy_static = "1.4.0"
toml = "0.5.6"
clap = { version = "^2", optional = true }
Expand Down
138 changes: 138 additions & 0 deletions src/hook.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use std::{fmt, process::Command, str::FromStr};

use crate::Result;

static ENTRY_SYMBOL: char = '%';

pub struct Hook(Vec<String>);

impl FromStr for Hook {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
bail!("hook must not be an empty string")
}

let words = shell_words::split(s)?;

Ok(Hook(words))
}
}

impl fmt::Display for Hook {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let command = shell_words::join(self.0.iter());
f.write_str(&command)
}
}

impl Hook {
pub fn entries(&mut self) -> impl Iterator<Item = HookEntry> {
self.0
.iter_mut()
.filter(|s| s.starts_with(ENTRY_SYMBOL))
.map(HookEntry)
}

pub fn is_ready(&self) -> bool {
!self.0.iter().any(|s| s.starts_with(ENTRY_SYMBOL))
}

pub fn run(&self) -> Result<()> {
// Hook should have all entries filled before running
assert!(self.is_ready());

let (cmd, args) = self.0.split_first().expect("hook must not be empty");

let status = Command::new(&cmd).args(args).status()?;

if !status.success() {
Err(anyhow!("hook failed with status {}", status))
} else {
Ok(())
}
}
}

pub struct HookEntry<'a>(&'a mut String);

impl HookEntry<'_> {
pub fn fill<'b, F>(&mut self, f: F) -> Result<()>
where
F: FnOnce(&str) -> Option<&'b str> + 'b,
{
// trim ENTRY_SYMBOL in the beginning
let key = &self.0[1..];

let value = f(key).ok_or_else(|| anyhow!("unknown key {}", key))?;

self.0.clear();
self.0.push_str(value);

Ok(())
}
}

#[cfg(test)]
mod test {
use super::Hook;
use crate::Result;
use std::str::FromStr;

#[test]
fn parse_empty_string() {
assert!(Hook::from_str("").is_err())
}

#[test]
fn parse_valid_string() -> Result<()> {
let hook = Hook::from_str("cargo bump %version")?;
assert_eq!(
&hook.0,
&["cargo".to_string(), "bump".into(), "%version".into()]
);
Ok(())
}

#[test]
fn fill_entries() -> Result<()> {
let mut hook = Hook::from_str("cmd %one %two %three")?;

assert!(!hook.is_ready());

hook.entries().try_for_each(|mut entry| {
entry.fill(|key| match key {
"one" => Some("1"),
"two" => Some("2"),
"three" => Some("3"),
_ => None,
})
})?;

assert!(hook.is_ready());

assert_eq!(
&hook.0,
&["cmd".to_string(), "1".into(), "2".into(), "3".into()]
);

Ok(())
}

#[test]
fn fill_entries_unknown_key() -> Result<()> {
let mut hook = Hook::from_str("%unknown")?;

assert!(!hook.is_ready());

let result = hook
.entries()
.try_for_each(|mut entry| entry.fill(|_| None));
assert!(result.is_err());

assert!(!hook.is_ready());

Ok(())
}
}
34 changes: 32 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod error;
pub mod filter;

pub mod commit;
pub mod hook;
pub mod repository;
pub mod settings;
pub mod version;
Expand All @@ -22,20 +23,21 @@ use crate::filter::CommitFilters;
use crate::repository::Repository;
use crate::settings::Settings;
use crate::version::{parse_pre_release, VersionIncrement};
use anyhow::Result;
use anyhow::{Context, Result};
use chrono::Utc;
use colored::*;
use commit::Commit;
use git2::{Oid, RebaseOptions};
use hook::Hook;
use semver::Version;
use serde::export::fmt::Display;
use serde::export::Formatter;
use settings::AuthorSetting;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{exit, Command, Stdio};
use std::{collections::HashMap, str::FromStr};
use tempdir::TempDir;

pub type CommitsMetadata = HashMap<CommitType, CommitConfig>;
Expand Down Expand Up @@ -393,6 +395,8 @@ impl CocoGitto {
.write()
.map_err(|err| anyhow!("Unable to write CHANGELOG.md : {}", err))?;

self.run_bump_hooks(&version_str)?;

self.repository.add_all()?;
self.repository
.commit(&format!("chore(version): {}", next_version))?;
Expand Down Expand Up @@ -496,6 +500,32 @@ impl CocoGitto {
.map_err(|err| anyhow!("`{}` is not a valid oid : {}", input, err))
}
}

fn run_bump_hooks(&self, next_version: &str) -> Result<()> {
let settings = Settings::get(&self.repository)?;

let hooks = settings
.hooks
.iter()
.map(String::as_str)
.map(Hook::from_str)
.enumerate()
.map(|(idx, result)| result.context(format!("Cannot parse hook at index {}", idx)))
.collect::<Result<Vec<Hook>>>()?;

for mut hook in hooks {
hook.entries().try_for_each(|mut entry| {
entry.fill(|key| match key {
"version" => Some(next_version),
_ => None,
})
})?;

hook.run().context(format!("{}", hook))?;
}

Ok(())
}
}

enum OidOf {
Expand Down
25 changes: 25 additions & 0 deletions tests/cog_bump_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,28 @@ fn pre_release_bump() -> Result<()> {

Ok(std::env::set_current_dir(current_dir)?)
}

#[test]
#[cfg(not(tarpaulin))]
#[cfg(target_os = "linux")]
fn bump_with_hook() -> Result<()> {
let current_dir = std::env::current_dir()?;
let mut command = Command::cargo_bin("cog")?;
command.arg("bump").arg("--major");

let temp_dir = TempDir::default();
std::env::set_current_dir(&temp_dir)?;

std::fs::write("cog.toml", r#"hooks = ["touch %version"]"#)?;

helper::git_init(".")?;
helper::git_commit("chore: init")?;
helper::git_tag("1.0.0")?;
helper::git_commit("feat: feature")?;

command.assert().success();
assert!(temp_dir.join("2.0.0").exists());
helper::assert_tag("2.0.0")?;

Ok(std::env::set_current_dir(current_dir)?)
}

0 comments on commit c11147d

Please sign in to comment.