use std::{
fs,
path::{Component, Path, PathBuf},
process::{Command, ExitCode},
};
use anyhow::{Context, Result};
use crate::{
cli,
hooks::{FORWARDER_NAME, FORWARDER_SOURCE, MANAGED_MARKER},
ledger::LedgerStore,
provenance,
reviewer::{ReviewQueue, ReviewRunStore},
};
const HOOKS: &[&str] = &["commit-msg", "post-commit", "pre-push"];
pub fn run(_args: cli::StatusArgs, state_dir: &Path) -> Result<ExitCode> {
let repo_root = git_root().context("this command requires a Git repository")?;
let resolved_state_dir = repo_relative_path(&repo_root, state_dir);
let hook_status = inspect_hook_status(&repo_root, state_dir);
let queue = ReviewQueue::new(&resolved_state_dir).summary();
let run_counts = ReviewRunStore::new(&resolved_state_dir).status_counts();
let blocking = LedgerStore::new(&resolved_state_dir).blocking_rejections();
println!("repo: {}", repo_root.display());
match hook_status {
Ok(hook_status) => {
println!("hook_mode: {}", hook_status.mode);
for hook in hook_status.hooks {
let state = if hook.installed {
"installed"
} else {
"missing"
};
println!("hook {}: {} path={}", hook.name, state, hook.path.display());
}
}
Err(error) => println!(
"hook_mode: unavailable warning={}",
single_line_error(error.as_ref())
),
}
match queue {
Ok(queue) => {
let oldest = queue
.oldest_age_secs_at(crate::time::unix_now())
.map_or_else(|| "none".to_owned(), |age| format!("{age}s"));
println!("queue: pending={} oldest_age={oldest}", queue.pending_count);
}
Err(error) => println!("queue: unavailable warning={}", single_line_error(&error)),
}
match run_counts {
Ok(run_counts) => {
let warning = if run_counts.skipped_records == 0 {
String::new()
} else {
format!(" warning=skipped_records:{}", run_counts.skipped_records)
};
println!(
"review_runs: queued={} running={} completed={} failed={} cancelled={}{}",
run_counts.queued,
run_counts.running,
run_counts.completed,
run_counts.failed,
run_counts.cancelled,
warning
);
}
Err(error) => println!(
"review_runs: unavailable warning={}",
single_line_error(&error)
),
}
println!("review_run_liveness: not_reconciled_read_only");
match blocking {
Ok(entries) => {
let needs_human = entries
.iter()
.filter(|entry| entry.is_needs_human())
.count();
println!(
"ledger: blocking_rejections={} (needs_human={})",
entries.len(),
needs_human
);
}
Err(error) => println!("ledger: unavailable warning={}", single_line_error(&error)),
}
if let Ok(head) = git_stdout(&repo_root, &["rev-parse", "HEAD"])
&& let Some(checkpoint) = provenance::entire_checkpoint_for_commit(&repo_root, head.trim())
{
println!(
"entire: ref={} sha={}",
checkpoint.ref_name, checkpoint.object_sha
);
} else {
println!("entire: none (optional)");
}
Ok(ExitCode::SUCCESS)
}
struct HookStatus {
mode: &'static str,
hooks: Vec<HookProbe>,
}
struct HookProbe {
name: &'static str,
path: PathBuf,
installed: bool,
}
fn inspect_hook_status(repo_root: &Path, state_dir: &Path) -> Result<HookStatus> {
let mode = detect_hook_mode(repo_root, state_dir)?;
let hooks = HOOKS
.iter()
.map(|hook| {
let path = active_hook_path(repo_root, &mode, hook)?;
let installed = hook_is_live_truth_mirror(repo_root, &path, hook, &mode);
Ok(HookProbe {
name: hook,
path,
installed,
})
})
.collect::<Result<Vec<_>>>()?;
Ok(HookStatus {
mode: mode.as_str(),
hooks,
})
}
enum StatusHookMode {
Plain,
LegacyTruthMirror {
hooks_path: PathBuf,
},
Husky {
content_dir: PathBuf,
entry_dir: PathBuf,
},
Custom {
hooks_path: PathBuf,
},
}
impl StatusHookMode {
fn as_str(&self) -> &'static str {
match self {
Self::Plain => "plain",
Self::LegacyTruthMirror { .. } => "plain-legacy-truth-mirror",
Self::Husky { .. } => "husky",
Self::Custom { .. } => "custom-committed",
}
}
}
fn detect_hook_mode(repo_root: &Path, state_dir: &Path) -> Result<StatusHookMode> {
let Some(configured) = git_config_get(repo_root, "core.hooksPath")? else {
return Ok(StatusHookMode::Plain);
};
let configured = configured.trim();
if configured.is_empty() {
return Ok(StatusHookMode::Plain);
}
let hooks_path = repo_relative_path(repo_root, Path::new(configured));
if is_managed_hooks_path(repo_root, state_dir, configured) {
return Ok(StatusHookMode::LegacyTruthMirror { hooks_path });
}
if configured.contains(".husky") {
let content_dir = if hooks_path.file_name().and_then(|name| name.to_str()) == Some("_") {
hooks_path
.parent()
.map_or_else(|| repo_root.join(".husky"), Path::to_path_buf)
} else {
hooks_path.clone()
};
return Ok(StatusHookMode::Husky {
content_dir,
entry_dir: hooks_path,
});
}
Ok(StatusHookMode::Custom { hooks_path })
}
fn active_hook_path(repo_root: &Path, mode: &StatusHookMode, hook: &str) -> Result<PathBuf> {
match mode {
StatusHookMode::Plain => {
let path = git_stdout(
repo_root,
&["rev-parse", "--git-path", &format!("hooks/{hook}")],
)?;
Ok(repo_relative_path(repo_root, Path::new(path.trim())))
}
StatusHookMode::LegacyTruthMirror { hooks_path } => Ok(hooks_path.join(hook)),
StatusHookMode::Husky { content_dir, .. } => Ok(content_dir.join(hook)),
StatusHookMode::Custom { hooks_path } => Ok(hooks_path.join(hook)),
}
}
fn hook_is_live_truth_mirror(
repo_root: &Path,
path: &Path,
hook: &str,
mode: &StatusHookMode,
) -> bool {
if let StatusHookMode::Custom { hooks_path } = mode {
return custom_hook_forwards_to_local(repo_root, hooks_path, path, hook);
}
if let StatusHookMode::Husky {
content_dir,
entry_dir,
} = mode
{
let content_is_active_entry = entry_dir == content_dir;
let content_hook_is_live = if !content_is_active_entry
&& husky_entry_runs_content_hook_with_shell(entry_dir, content_dir, hook)
{
truth_mirror_hook_content_is_live(path, hook)
} else {
direct_truth_mirror_hook_is_live(path, hook)
};
return content_hook_is_live
&& (content_is_active_entry
|| husky_entry_forwards_to_content(entry_dir, content_dir, hook));
}
direct_truth_mirror_hook_is_live(path, hook)
}
fn direct_truth_mirror_hook_is_live(path: &Path, hook: &str) -> bool {
hook_is_executable_with_content(path, |content| {
hook_content_installs_truth_mirror(content, hook, &StatusHookMode::Plain)
})
}
fn truth_mirror_hook_content_is_live(path: &Path, hook: &str) -> bool {
fs::read_to_string(path).is_ok_and(|content| {
hook_content_installs_truth_mirror(&content, hook, &StatusHookMode::Plain)
})
}
fn hook_is_executable_with_content(path: &Path, predicate: impl FnOnce(&str) -> bool) -> bool {
is_executable_file(path) && fs::read_to_string(path).is_ok_and(|content| predicate(&content))
}
fn hook_content_installs_truth_mirror(content: &str, hook: &str, mode: &StatusHookMode) -> bool {
match mode {
StatusHookMode::Custom { .. } => {
content.contains(MANAGED_MARKER)
&& content.contains(FORWARDER_NAME)
&& content.contains(hook)
}
StatusHookMode::Plain
| StatusHookMode::LegacyTruthMirror { .. }
| StatusHookMode::Husky { .. } => {
active_hook_lines(content).any(|line| line_invokes_truth_hook_dispatch(line, hook))
}
}
}
fn active_hook_lines(content: &str) -> impl Iterator<Item = &str> {
content
.lines()
.map(str::trim_start)
.filter(|line| !line.trim().is_empty() && !line.starts_with('#'))
}
fn line_invokes_truth_hook_dispatch(line: &str, hook: &str) -> bool {
crate::shell::shellish_token_segments(line)
.iter()
.any(|tokens| token_segment_invokes_truth_hook_dispatch(tokens, hook))
}
fn token_segment_invokes_truth_hook_dispatch(tokens: &[&str], hook: &str) -> bool {
for (index, token) in tokens.iter().enumerate() {
if !is_truth_binary(token) {
continue;
}
let Some(dispatch_offset) = tokens[index + 1..]
.iter()
.position(|candidate| *candidate == "hook-dispatch")
else {
continue;
};
let hook_index = index + 1 + dispatch_offset + 1;
if tokens
.get(hook_index)
.is_some_and(|candidate| *candidate == hook)
{
return true;
}
}
false
}
fn is_truth_binary(token: &str) -> bool {
token == "truth"
|| token == "truth-mirror"
|| token.ends_with("/truth")
|| token.ends_with("/truth-mirror")
}
fn custom_forwarder_helper_is_live(hooks_path: &Path) -> bool {
hook_is_executable_with_content(&hooks_path.join(FORWARDER_NAME), |content| {
content == FORWARDER_SOURCE
})
}
fn custom_hook_forwards_to_local(
repo_root: &Path,
hooks_path: &Path,
path: &Path,
hook: &str,
) -> bool {
if direct_truth_mirror_hook_is_live(path, hook) {
return true;
}
let active_forwards = hook_is_executable_with_content(path, |content| {
if hook_content_installs_truth_mirror(
content,
hook,
&StatusHookMode::Custom {
hooks_path: hooks_path.to_path_buf(),
},
) {
return custom_forwarder_helper_is_live(hooks_path);
}
content_forwards_to_local_git_hook(content, hook)
});
active_forwards
&& forwarded_local_hook_path(repo_root, hook)
.is_ok_and(|local_hook| direct_truth_mirror_hook_is_live(&local_hook, hook))
}
fn content_forwards_to_local_git_hook(content: &str, hook: &str) -> bool {
let resolves_git_dir =
content.contains("--git-common-dir") || content.contains("git rev-parse --git-common-dir");
resolves_git_dir
&& active_hook_lines(content).any(|line| {
line.contains("$@")
&& (line.contains(&format!("hooks/{hook}"))
|| line.contains("hooks/$name")
|| line.contains("hooks/${name}")
|| line.contains("hooks/$hook")
|| line.contains("hooks/${hook}"))
})
}
fn husky_entry_forwards_to_content(entry_dir: &Path, content_dir: &Path, hook: &str) -> bool {
let entry = entry_dir.join(hook);
hook_is_executable_with_content(&entry, |content| {
if content_mentions_husky_content_hook(content, hook) {
return true;
}
if content_mentions_husky_helper(content) {
return husky_helper_forwards_to_content(
entry_dir,
content_dir,
!content_sources_husky_helper(content),
);
}
false
})
}
fn husky_entry_runs_content_hook_with_shell(
entry_dir: &Path,
content_dir: &Path,
hook: &str,
) -> bool {
let entry = entry_dir.join(hook);
hook_is_executable_with_content(&entry, |content| {
if content_mentions_husky_helper(content) {
return husky_helper_runs_content_hook_with_shell(
entry_dir,
content_dir,
!content_sources_husky_helper(content),
);
}
false
})
}
fn husky_helper_forwards_to_content(
entry_dir: &Path,
content_dir: &Path,
require_executable: bool,
) -> bool {
if normalize_path(&entry_dir.join("..")) != normalize_path(content_dir) {
return false;
}
let helper = entry_dir.join("h");
if require_executable && !is_executable_file(&helper) {
return false;
}
fs::read_to_string(helper).is_ok_and(|content| {
let active_content = active_hook_lines(&content).collect::<Vec<_>>().join("\n");
let names_active_hook = active_content.contains("hook_name")
|| active_content.contains("basename \"$0\"")
|| active_content.contains("${0##*/}");
let targets_parent_hook = active_content.contains("content_dir")
|| active_content.contains("dirname \"$(dirname \"$0\")\"")
|| active_content.contains("${0%/*/*}");
names_active_hook && targets_parent_hook && active_content.contains("\"$@\"")
})
}
fn husky_helper_runs_content_hook_with_shell(
entry_dir: &Path,
content_dir: &Path,
require_executable: bool,
) -> bool {
if !husky_helper_forwards_to_content(entry_dir, content_dir, require_executable) {
return false;
}
fs::read_to_string(entry_dir.join("h")).is_ok_and(|content| {
active_hook_lines(&content).any(|line| {
line.contains("sh -e ") || line.contains("sh \"$") || line.contains("sh '$")
})
})
}
fn content_mentions_husky_helper(content: &str) -> bool {
active_hook_lines(content).any(|line| line.contains("/h\"") || line.contains("/h'"))
}
fn content_sources_husky_helper(content: &str) -> bool {
active_hook_lines(content)
.any(|line| line.starts_with(". ") && (line.contains("/h\"") || line.contains("/h'")))
}
fn content_mentions_husky_content_hook(content: &str, hook: &str) -> bool {
active_hook_lines(content)
.any(|line| line.contains(&format!("/{hook}\"")) || line.contains(&format!("/{hook}'")))
}
fn forwarded_local_hook_path(repo_root: &Path, hook: &str) -> Result<PathBuf> {
let git_dir = git_stdout(repo_root, &["rev-parse", "--git-common-dir"])?;
Ok(repo_relative_path(repo_root, Path::new(git_dir.trim()))
.join("hooks")
.join(hook))
}
fn single_line_error(error: &dyn std::error::Error) -> String {
error.to_string().replace(['\r', '\n'], " ")
}
#[cfg(unix)]
fn is_executable_file(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
fs::metadata(path)
.is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
}
#[cfg(not(unix))]
fn is_executable_file(path: &Path) -> bool {
path.is_file()
}
fn repo_relative_path(repo_root: &Path, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
repo_root.join(path)
}
}
fn is_managed_hooks_path(repo_root: &Path, state_dir: &Path, configured: &str) -> bool {
let configured_path = normalize_path(&repo_relative_path(repo_root, Path::new(configured)));
[
state_dir.join("hooks"),
PathBuf::from(crate::config::DEFAULT_STATE_DIR).join("hooks"),
PathBuf::from(crate::config::LEGACY_STATE_DIR).join("hooks"),
]
.iter()
.any(|candidate| normalize_path(&repo_relative_path(repo_root, candidate)) == configured_path)
}
fn normalize_path(path: &Path) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if normalized.as_os_str().is_empty()
|| (!normalized.has_root() && normalized.ends_with(".."))
{
normalized.push("..");
} else {
normalized.pop();
}
}
Component::Normal(part) => normalized.push(part),
Component::RootDir | Component::Prefix(_) => normalized.push(component.as_os_str()),
}
}
normalized
}
fn git_root() -> Result<PathBuf> {
Ok(PathBuf::from(
git_stdout(Path::new("."), &["rev-parse", "--show-toplevel"])?.trim(),
))
}
fn git_config_get(repo_root: &Path, key: &str) -> Result<Option<String>> {
let output = Command::new("git")
.args(["config", "--get", key])
.current_dir(repo_root)
.output()?;
if output.status.success() {
return Ok(Some(String::from_utf8_lossy(&output.stdout).into_owned()));
}
if output.status.code() == Some(1) {
return Ok(None);
}
anyhow::bail!(
"git config --get {key} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn git_stdout(repo_root: &Path, args: &[&str]) -> Result<String> {
let output = Command::new("git")
.args(args)
.current_dir(repo_root)
.output()?;
if !output.status.success() {
anyhow::bail!(
"git {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&output.stderr)
);
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
#[test]
fn normalize_path_preserves_unmatched_relative_parents() {
assert_eq!(
super::normalize_path(Path::new("../hooks")),
PathBuf::from("../hooks")
);
assert_eq!(
super::normalize_path(Path::new("a/../../hooks")),
PathBuf::from("../hooks")
);
}
}