use crate::diagnostic_log::debug;
use crate::diagnostics::render_configuration_warning;
use crate::config_snapshot::{ConfigSnapshot, Lookup};
use std::io::Write;
pub const EXTERNAL_HOOKS_UNSUPPORTED_CODE: &str = "external-hooks-unsupported";
const TAG: &str = "externalHooks";
const HOOK_PREFIX: &str = "hook.";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfiguredHook {
pub name: String,
pub events: Vec<String>,
pub has_command: bool,
}
fn lossy(value: Option<&Vec<u8>>) -> String {
return match value {
Some(bytes) => String::from_utf8_lossy(bytes.as_slice()).into_owned(),
None => String::new(),
};
}
pub fn configured_hooks(snapshot: &ConfigSnapshot) -> Vec<ConfiguredHook> {
let mut hooks: Vec<ConfiguredHook> = Vec::new();
for entry in snapshot.entries() {
let Some(rest) = entry.key.strip_prefix(HOOK_PREFIX) else {
continue;
};
let (name, event_key): (&str, bool) = if let Some(name) = rest.strip_suffix(".event") {
(name, true)
} else if let Some(name) = rest.strip_suffix(".command") {
(name, false)
} else {
continue;
};
if name.is_empty() {
continue;
}
let mut known_position: Option<usize> = None;
for (index, known) in hooks.iter().enumerate() {
if known.name == name {
known_position = Some(index);
break;
}
}
let position: usize = match known_position {
Some(found) => found,
None => {
hooks.push(ConfiguredHook { name: String::from(name), events: Vec::new(), has_command: false });
hooks.len() - 1
}
};
if event_key {
let value: String = lossy(entry.value.as_ref());
if value.is_empty() {
hooks[position].events.clear();
} else {
hooks[position].events.push(value);
}
} else {
hooks[position].has_command = true;
}
}
let mut enabled: Vec<ConfiguredHook> = Vec::new();
for hook in hooks {
if !matches!(snapshot.bool_value(format!("hook.{}.enabled", hook.name).as_str()), Lookup::Found(false)) {
enabled.push(hook);
}
}
return enabled;
}
pub fn describe_hook(hook: &ConfiguredHook) -> String {
if hook.events.is_empty() {
return format!("hook.{} (no event)", hook.name);
}
return format!("hook.{} (events: {})", hook.name, hook.events.join(", "));
}
pub fn explain_external_hooks(hooks: &[ConfiguredHook]) -> String {
let listed: Vec<String> = hooks.iter().map(describe_hook).collect();
return format!(
"This repository configures external hooks: {}. monochromatic-git-policy-cli supports no external hook program, so every Git it \
starts has every hook event switched off and these never run. Ways forward: remove them with \
`git config --unset-all hook.<name>.event` and `git config --unset-all hook.<name>.command` for each name \
(the keys may live in a global or system file; `git config --show-origin --get-regexp '^hook[.]'` shows \
where), or express what the hook checked as a monochromatic-git-policy-cli policy under policies in monochromatic-git-policy-cli.config.jsonc. monochromatic-git-policy-cli \
runs no program named by any configuration, so no setting can bring a hook back. `git monochromatic-git-policy-cli doctor` lists \
these hooks.",
listed.join("; ")
);
}
pub fn external_hooks_warning(snapshot: Option<&ConfigSnapshot>) -> Option<String> {
let hooks: Vec<ConfiguredHook> = configured_hooks(snapshot?);
if hooks.is_empty() {
return None;
}
let mut names: Vec<String> = Vec::new();
for hook in &hooks {
names.push(format!("hook.{}", hook.name));
}
return Some(render_configuration_warning(
0,
EXTERNAL_HOOKS_UNSUPPORTED_CODE,
explain_external_hooks(hooks.as_slice()).as_str(),
names.join(",").as_str(),
));
}
pub fn warn_external_hooks(snapshot: Option<&ConfigSnapshot>, stream: &mut impl Write) {
let Some(line) = external_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 = "external_hooks_tests.rs"]
mod tests;