use anyhow::{bail, Context, Result};
use pushkin_core::manifest::Manifest;
use super::MANIFEST_FILE;
pub const FEATURE_GIT_HOOKS: &str = "git-hooks";
pub fn git_hooks_disabled() -> Result<bool> {
let text = match std::fs::read_to_string(MANIFEST_FILE) {
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(error) => return Err(error).with_context(|| format!("cannot read {MANIFEST_FILE}")),
Ok(text) => text,
};
let manifest = Manifest::parse(&text).context("manifest rejected")?;
Ok(!manifest.git_hooks_enabled())
}
pub fn ensure_git_hooks_enabled() -> Result<()> {
if git_hooks_disabled()? {
bail!(
"git hooks are disabled for this repo ([features] git_hooks = false \
in {MANIFEST_FILE}); run `pushkin enable git-hooks` first"
);
}
Ok(())
}
pub fn run_enable(feature: &str) -> Result<i32> {
require_known(feature)?;
set_git_hooks(true)?;
println!(
"pushkin: git hooks enabled ([features] git_hooks = true in {MANIFEST_FILE}). \
Nothing was installed: run `pushkin init --agent lefthook` (config-file \
floor) or `pushkin init --agent git` (native shim) to install the \
pre-commit gate."
);
Ok(0)
}
pub fn run_disable(feature: &str) -> Result<i32> {
require_known(feature)?;
set_git_hooks(false)?;
super::init::remove_lefthook()?;
super::init::remove_git_shim()?;
println!(
"pushkin: git hooks disabled ([features] git_hooks = false in {MANIFEST_FILE}); \
`init --agent lefthook|git` will refuse until re-enabled, doctor stops \
checking the floor, and `check --staged` passes with a notice. Agent \
write-time gates are unaffected. Re-enable with `pushkin enable git-hooks`."
);
Ok(0)
}
fn require_known(feature: &str) -> Result<()> {
if feature != FEATURE_GIT_HOOKS {
bail!("unknown feature '{feature}' (known features: {FEATURE_GIT_HOOKS})");
}
Ok(())
}
fn set_git_hooks(enabled: bool) -> Result<()> {
let text = std::fs::read_to_string(MANIFEST_FILE).with_context(|| {
format!(
"cannot read {MANIFEST_FILE} in the current directory — the feature \
flag lives in the manifest, so there is nothing to record without one"
)
})?;
let updated = splice_git_hooks(&text, enabled);
Manifest::parse(&updated).context(
"refusing to write: the updated manifest would be rejected — \
fix pushkin.toml by hand",
)?;
std::fs::write(MANIFEST_FILE, updated).with_context(|| format!("cannot write {MANIFEST_FILE}"))
}
fn splice_git_hooks(text: &str, enabled: bool) -> String {
let value_line = format!("git_hooks = {enabled}");
let lines: Vec<&str> = text.lines().collect();
let header = lines.iter().position(|line| line.trim() == "[features]");
let Some(header) = header else {
let separator = if text.is_empty() || text.ends_with("\n\n") {
""
} else if text.ends_with('\n') {
"\n"
} else {
"\n\n"
};
return format!("{text}{separator}[features]\n{value_line}\n");
};
let table_end = lines[header + 1..]
.iter()
.position(|line| line.trim_start().starts_with('['))
.map_or(lines.len(), |offset| header + 1 + offset);
let existing = lines[header + 1..table_end].iter().position(|line| {
let trimmed = line.trim_start();
trimmed
.strip_prefix("git_hooks")
.is_some_and(|rest| rest.trim_start().starts_with('='))
});
let mut out: Vec<String> = lines.iter().map(ToString::to_string).collect();
match existing {
Some(offset) => out[header + 1 + offset] = value_line,
None => out.insert(header + 1, value_line),
}
let mut joined = out.join("\n");
if text.ends_with('\n') {
joined.push('\n');
}
joined
}