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 value.map(|bytes| { return String::from_utf8_lossy(bytes.as_slice()).into_owned(); }).unwrap_or_default();
}
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 position: usize = match hooks.iter().position(|known| { return known.name == name; }) {
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;
}
}
hooks.retain(|hook| {
return !matches!(snapshot.bool_value(format!("hook.{}.enabled", hook.name).as_str()), Lookup::Found(false));
});
return hooks;
}
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: {}. cli-git 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 cli-git policy under policies in cli-git.config.jsonc. cli-git \
runs no program named by any configuration, so no setting can bring a hook back. `git cli-git 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 names: Vec<String> = hooks.iter().map(|hook| { return format!("hook.{}", hook.name); }).collect();
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;
};
if let Err(error) = stream.write_all(line.as_bytes()).and_then(|()| { return stream.flush(); }) {
debug(TAG, format!("the warning could not be written: {error}").as_str());
}
}
#[cfg(test)]
#[path = "external_hooks_tests.rs"]
mod tests;