use crate::{
command::{self, Error as CommandError, Output},
logging::LogExt,
vcs::{RevisionInfo, TagAndRevision},
version::Version,
};
use async_process::Command;
use std::collections::HashMap;
use std::path::Path;
pub const ENV_PREFIX: &str = "BVHOOK_";
fn base_env() -> impl Iterator<Item = (String, String)> {
vec![
(
format!("{ENV_PREFIX}NOW"),
chrono::Local::now().to_rfc3339(),
),
(
format!("{ENV_PREFIX}UTCNOW"),
chrono::Utc::now().to_rfc3339(),
),
]
.into_iter()
}
fn vcs_env(tag_and_revision: &TagAndRevision) -> impl Iterator<Item = (String, String)> {
let TagAndRevision { tag, revision } = tag_and_revision;
let tag = tag.clone().unwrap_or(crate::vcs::TagInfo {
dirty: false,
commit_sha: String::new(),
distance_to_latest_tag: 0,
current_tag: String::new(),
current_version: String::new(),
});
let revision = revision.clone().unwrap_or(RevisionInfo {
branch_name: String::new(),
short_branch_name: String::new(),
repository_root: std::path::PathBuf::default(),
});
vec![
(format!("{ENV_PREFIX}COMMIT_SHA"), tag.commit_sha),
(
format!("{ENV_PREFIX}DISTANCE_TO_LATEST_TAG"),
tag.distance_to_latest_tag.to_string(),
),
(format!("{ENV_PREFIX}IS_DIRTY"), tag.dirty.to_string()),
(format!("{ENV_PREFIX}CURRENT_VERSION"), tag.current_version),
(format!("{ENV_PREFIX}CURRENT_TAG"), tag.current_tag),
(format!("{ENV_PREFIX}BRANCH_NAME"), revision.branch_name),
(
format!("{ENV_PREFIX}SHORT_BRANCH_NAME"),
revision.short_branch_name,
),
]
.into_iter()
}
fn version_env<'a>(
version: Option<&'a Version>,
version_prefix: &'a str,
) -> impl Iterator<Item = (String, String)> + use<'a> {
let iter = version.map(|version| version.iter()).unwrap_or_default();
iter.map(move |(comp_name, comp)| {
(
format!("{ENV_PREFIX}{version_prefix}{}", comp_name.to_uppercase()),
comp.value().unwrap_or_default().to_string(),
)
})
}
fn new_version_env<'a>(
new_version_serialized: &str,
tag: Option<&str>,
) -> impl Iterator<Item = (String, String)> + use<'a> {
vec![
(
format!("{ENV_PREFIX}NEW_VERSION"),
new_version_serialized.to_string(),
),
(
format!("{ENV_PREFIX}NEW_VERSION_TAG"),
tag.unwrap_or_default().to_string(),
),
]
.into_iter()
}
fn setup_hook_env<'a>(
tag_and_revision: &'a TagAndRevision,
current_version: Option<&'a Version>,
) -> impl Iterator<Item = (String, String)> + use<'a> {
std::env::vars()
.chain(base_env())
.chain(vcs_env(tag_and_revision))
.chain(version_env(current_version, "CURRENT_"))
}
fn pre_and_post_commit_hook_env<'a>(
tag_and_revision: &'a TagAndRevision,
current_version: Option<&'a Version>,
new_version: Option<&'a Version>,
new_version_serialized: &str,
) -> impl Iterator<Item = (String, String)> + use<'a> {
let tag = tag_and_revision
.tag
.as_ref()
.map(|tag| tag.current_tag.as_str());
std::env::vars()
.chain(base_env())
.chain(vcs_env(tag_and_revision))
.chain(version_env(current_version, "CURRENT_"))
.chain(version_env(new_version, "NEW_"))
.chain(new_version_env(new_version_serialized, tag))
}
impl<VCS, L> crate::BumpVersion<VCS, L>
where
VCS: crate::vcs::VersionControlSystem,
L: crate::logging::Log,
{
pub async fn run_setup_hooks(&self, current_version: Option<&Version>) -> Result<(), Error> {
let env = setup_hook_env(&self.tag_and_revision, current_version);
let setup_hooks = &self.config.global.setup_hooks;
self.logger.log_hooks("setup", setup_hooks);
run_hooks(
setup_hooks,
self.repo.path(),
env,
self.config.global.dry_run,
)
.await
}
pub async fn run_pre_commit_hooks(
&self,
current_version: Option<&Version>,
new_version: Option<&Version>,
new_version_serialized: &str,
) -> Result<(), Error> {
let env = pre_and_post_commit_hook_env(
&self.tag_and_revision,
current_version,
new_version,
new_version_serialized,
);
let pre_commit_hooks = &self.config.global.pre_commit_hooks;
self.logger.log_hooks("pre-commit", pre_commit_hooks);
run_hooks(
pre_commit_hooks,
self.repo.path(),
env,
self.config.global.dry_run,
)
.await
}
pub async fn run_post_commit_hooks(
&self,
current_version: Option<&Version>,
new_version: Option<&Version>,
new_version_serialized: &str,
) -> Result<(), Error> {
let env = pre_and_post_commit_hook_env(
&self.tag_and_revision,
current_version,
new_version,
new_version_serialized,
);
let post_commit_hooks = &self.config.global.post_commit_hooks;
self.logger.log_hooks("post-commit", post_commit_hooks);
run_hooks(
post_commit_hooks,
self.repo.path(),
env,
self.config.global.dry_run,
)
.await
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Command(#[from] CommandError),
#[error("failed to split shell script {0:?}")]
Shell(String),
}
async fn run_hook(
script: &str,
working_dir: &Path,
env: &HashMap<String, String>,
) -> Result<Output, Error> {
let args = shlex::split(script).ok_or_else(|| Error::Shell(script.to_string()))?;
let mut cmd = Command::new("sh");
cmd.args(["-c".to_string()].into_iter().chain(args));
cmd.envs(env);
cmd.current_dir(working_dir);
let output = command::run_command(&mut cmd).await?;
Ok(output)
}
async fn run_hooks(
hooks: &[String],
working_dir: &Path,
env: impl Iterator<Item = (String, String)>,
dry_run: bool,
) -> Result<(), Error> {
let env = env.collect();
for script in hooks {
if dry_run {
tracing::info!(?script, "would run hook");
continue;
}
tracing::info!(?script, "running");
match run_hook(script, working_dir, &env).await {
Ok(output) => {
tracing::debug!(code = output.status.code(), "hook completed");
tracing::debug!(output.stdout);
tracing::debug!(output.stderr);
}
Err(err) => {
if let Error::Command(CommandError::Failed { ref output, .. }) = err {
tracing::warn!(output.stdout);
tracing::warn!(output.stderr);
}
return Err(err);
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn test_current_version_env_includes_correct_info() {
}
}