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 shadow
.parent()
.and_then(Path::file_name)
.is_some_and(|root| { return root == SHADOW_ROOT_NAME; });
}
pub fn configured_hooks_path(snapshot: &ConfigSnapshot) -> Option<String> {
let entry = snapshot
.entries()
.iter()
.rev()
.find(|candidate| { return candidate.key == HOOKS_PATH_KEY; })?;
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). cli-git 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 cli-git.config.jsonc instead. \
Run `git cli-git 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;
};
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 = "traditional_hook_warning_tests.rs"]
mod tests;