use crate::diagnostic_log::debug;
use crate::diagnostics::render_configuration_warning;
use crate::config_snapshot::ConfigSnapshot;
use crate::hooks_off::NO_HOOKS_DIRECTORY;
use crate::shadow_git::{SHADOW_NO_HOOKS_NAME, SHADOW_ROOT_NAME};
use crate::random_id::is_canonical_uuid;
use std::io::Write;
use std::ffi::OsStr;
use std::path::Path;
pub const TRADITIONAL_HOOKS_UNSUPPORTED_CODE: &str = "traditional-hooks-unsupported";
const HOOKS_PATH_KEY: &str = "core.hookspath";
const TAG: &str = "traditionalHookWarning";
pub fn is_wrapper_hooks_path(value: &str) -> bool {
if value == NO_HOOKS_DIRECTORY {
return true;
}
let path: &Path = Path::new(value);
let Some(name) = path.file_name().and_then(OsStr::to_str) else {
return false;
};
if name != SHADOW_NO_HOOKS_NAME {
return false;
}
let Some(shadow) = path.parent() else {
return false;
};
let Some(identifier) = shadow.file_name().and_then(OsStr::to_str) else {
return false;
};
if !is_canonical_uuid(identifier) {
return false;
}
return match shadow.parent().and_then(Path::file_name) {
Some(root) => root == SHADOW_ROOT_NAME,
None => false,
};
}
pub fn configured_hooks_path(snapshot: &ConfigSnapshot) -> Option<String> {
let mut last_entry = None;
for candidate in snapshot.entries().iter().rev() {
if candidate.key == HOOKS_PATH_KEY {
last_entry = Some(candidate);
break;
}
}
let entry = last_entry?;
let bytes: &[u8] = entry.value.as_deref()?;
if bytes.is_empty() {
return None;
}
let text: String = String::from_utf8_lossy(bytes).into_owned();
if is_wrapper_hooks_path(text.as_str()) {
return None;
}
return Some(text);
}
pub fn traditional_hooks_warning(snapshot: Option<&ConfigSnapshot>) -> Option<String> {
let value: String = configured_hooks_path(snapshot?)?;
let message: String = format!(
"`core.hooksPath` is set to {value}, so this repository may use traditional Git hooks (executable scripts in \
a hooks directory). monochromatic-git-policy-cli supports no external hook program, traditional or configured: every Git it \
starts has the hooks directory switched off, so these scripts never run. Express what a script checked as a \
policy under policies in monochromatic-git-policy-cli.config.jsonc instead. \
Run `git monochromatic-git-policy-cli doctor` to list the traditional hooks it can find."
);
return Some(render_configuration_warning(
0,
TRADITIONAL_HOOKS_UNSUPPORTED_CODE,
message.as_str(),
value.as_str(),
));
}
pub fn warn_traditional_hooks(snapshot: Option<&ConfigSnapshot>, stream: &mut impl Write) {
let Some(line) = traditional_hooks_warning(snapshot) else {
return;
};
let written: std::io::Result<()> = match stream.write_all(line.as_bytes()) {
Ok(()) => stream.flush(),
Err(error) => Err(error),
};
if let Err(error) = written {
debug(TAG, format!("the warning could not be written: {error}").as_str());
}
}
#[cfg(test)]
#[path = "traditional_hook_warning_tests.rs"]
mod tests;