pub(crate) mod classify;
pub(crate) mod handlers;
pub(crate) mod real_git;
pub(crate) mod shadow;
use std::path::PathBuf;
use std::process::ExitCode;
use crate::agent_detection::EnvSource;
use crate::metrics::{ShimOutcome, ShimSubcommand};
use crate::shim::classify::{Classification, classify};
use crate::shim::real_git::RealGitExec;
pub(crate) fn run_shim<E: EnvSource, G: RealGitExec>(argv: &[&str], env: &E, exec: &G) -> ExitCode {
let metrics = crate::metrics::get();
if env.get("GIT_PRISM_INSIDE_SHIM").is_some() {
metrics.record_shim_invocation(ShimOutcome::LoopBreak);
return exec.passthrough(argv);
}
if crate::agent_detection::detect_calling_agent(env).is_none() {
metrics.record_shim_invocation(ShimOutcome::NoAgent);
return exec.passthrough(argv);
}
let classification = classify(argv);
let subcommand = classification_to_subcommand(&classification);
if classification == Classification::Passthrough {
metrics.record_shim_invocation(ShimOutcome::Passthrough);
return exec.passthrough(argv);
}
let repo_path = match resolve_repo_path(env) {
Some(p) => p,
None => {
metrics.record_shim_invocation(ShimOutcome::Passthrough);
return exec.passthrough(argv);
}
};
metrics.record_shim_classification(subcommand);
let mut out_buf = Vec::new();
let code = handlers::handle(&classification, &repo_path, &mut out_buf);
metrics.record_shim_invocation(ShimOutcome::Structured);
metrics.record_shim_response_bytes(out_buf.len() as u64);
use std::io::Write;
if let Err(e) = std::io::stdout().write_all(&out_buf) {
tracing::warn!(error = %e, "failed to write structured response to stdout");
}
shadow::maybe_shadow_capture(env, subcommand, argv, exec);
code
}
fn classification_to_subcommand(c: &Classification<'_>) -> ShimSubcommand {
match c {
Classification::Manifest { .. } => ShimSubcommand::Diff,
Classification::History { .. } => ShimSubcommand::Log,
Classification::FunctionContext { .. } => ShimSubcommand::Log, Classification::ShowSnapshot { .. } => ShimSubcommand::Show,
Classification::BlameSnapshot { .. } => ShimSubcommand::Blame,
Classification::GhPrDiff { .. } => ShimSubcommand::Diff,
Classification::Passthrough => ShimSubcommand::Other,
}
}
fn resolve_repo_path(env: &dyn EnvSource) -> Option<PathBuf> {
if let Some(repo) = env.get("GIT_PRISM_REPO") {
return Some(PathBuf::from(repo));
}
if env.get("GIT_PRISM_CWD_UNAVAILABLE").is_some() {
return None;
}
std::env::current_dir().ok()
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::process::ExitCode;
use super::*;
struct MapEnv(HashMap<&'static str, &'static str>);
impl EnvSource for MapEnv {
fn get(&self, key: &str) -> Option<String> {
self.0.get(key).map(|v| v.to_string())
}
}
struct SpyExec {
pub called: std::cell::Cell<bool>,
pub exit_code: ExitCode,
}
impl SpyExec {
fn new(exit_code: ExitCode) -> Self {
Self {
called: std::cell::Cell::new(false),
exit_code,
}
}
}
impl RealGitExec for SpyExec {
fn passthrough(&self, _argv: &[&str]) -> ExitCode {
self.called.set(true);
self.exit_code
}
fn capture(&self, _argv: &[&str]) -> Result<usize, crate::shim::real_git::CaptureError> {
Ok(0)
}
}
#[test]
fn it_passes_through_when_inside_shim_sentinel_is_set() {
let env = MapEnv(HashMap::from([
("GIT_PRISM_INSIDE_SHIM", "1"),
("CLAUDECODE", "1"),
]));
let exec = SpyExec::new(ExitCode::SUCCESS);
run_shim(&["git", "diff", "main..HEAD"], &env, &exec);
assert!(
exec.called.get(),
"expected passthrough when sentinel is set"
);
}
#[test]
fn it_passes_through_when_no_agent_env_var_is_set() {
let env = MapEnv(HashMap::new());
let exec = SpyExec::new(ExitCode::SUCCESS);
run_shim(&["git", "diff", "main..HEAD"], &env, &exec);
assert!(
exec.called.get(),
"expected passthrough when no agent env var is set"
);
}
#[test]
fn it_passes_through_when_subcommand_is_not_on_watch_list() {
let env = MapEnv(HashMap::from([("CLAUDECODE", "1")]));
let exec = SpyExec::new(ExitCode::SUCCESS);
run_shim(&["git", "status"], &env, &exec);
assert!(
exec.called.get(),
"expected passthrough for unrecognised subcommand"
);
}
#[test]
fn it_passes_through_when_sentinel_takes_priority_over_agent_detection() {
let env = MapEnv(HashMap::from([
("GIT_PRISM_INSIDE_SHIM", "1"),
("CLAUDECODE", "1"),
]));
let exec = SpyExec::new(ExitCode::SUCCESS);
run_shim(&["git", "diff", "main..HEAD"], &env, &exec);
assert!(
exec.called.get(),
"sentinel must take priority over agent detection"
);
}
#[test]
fn it_dispatches_to_handler_when_agent_set_and_subcommand_classified() {
use std::process::Command;
use tempfile::TempDir;
let dir = TempDir::new().unwrap();
let repo_path = dir.path().to_path_buf();
let run = |args: &[&str]| {
Command::new("git")
.args(args)
.current_dir(&repo_path)
.output()
.unwrap()
};
run(&["init", "-b", "main"]);
run(&["config", "user.email", "t@t.com"]);
run(&["config", "user.name", "T"]);
std::fs::write(repo_path.join("a.txt"), "hello\n").unwrap();
run(&["add", "a.txt"]);
run(&["commit", "-m", "first"]);
std::fs::write(repo_path.join("b.txt"), "world\n").unwrap();
run(&["add", "b.txt"]);
run(&["commit", "-m", "second"]);
let repo_str: &'static str =
Box::leak(repo_path.to_string_lossy().into_owned().into_boxed_str());
let env = MapEnv(HashMap::from([
("CLAUDECODE", "1"),
("GIT_PRISM_REPO", repo_str),
]));
let exec = SpyExec::new(ExitCode::SUCCESS);
let code = run_shim(&["git", "diff", "HEAD~1..HEAD"], &env, &exec);
assert!(
!exec.called.get(),
"expected handler dispatch, not passthrough"
);
assert_eq!(code, ExitCode::SUCCESS, "handler should return SUCCESS");
}
#[test]
fn record_shim_invocation_and_classification_do_not_panic_in_sequence() {
let metrics = crate::metrics::Metrics::new_for_test();
metrics.record_shim_classification(crate::metrics::ShimSubcommand::Diff);
metrics.record_shim_invocation(crate::metrics::ShimOutcome::Structured);
metrics.record_shim_invocation(crate::metrics::ShimOutcome::Passthrough);
metrics.record_shim_invocation(crate::metrics::ShimOutcome::LoopBreak);
metrics.record_shim_invocation(crate::metrics::ShimOutcome::NoAgent);
}
#[test]
fn it_passes_through_when_current_dir_is_unavailable() {
let env = MapEnv(HashMap::from([
("CLAUDECODE", "1"),
("GIT_PRISM_CWD_UNAVAILABLE", "1"),
]));
let exec = SpyExec::new(ExitCode::SUCCESS);
run_shim(&["git", "diff", "main..HEAD"], &env, &exec);
assert!(
exec.called.get(),
"expected passthrough when current directory cannot be determined"
);
}
#[test]
fn classification_to_subcommand_maps_each_variant() {
use crate::shim::classify::Classification;
assert_eq!(
classification_to_subcommand(&Classification::Manifest { range: "x" }),
ShimSubcommand::Diff
);
assert_eq!(
classification_to_subcommand(&Classification::History { range: "x" }),
ShimSubcommand::Log
);
assert_eq!(
classification_to_subcommand(&Classification::FunctionContext {
range: None,
pickaxe_term: "x",
}),
ShimSubcommand::Log
);
assert_eq!(
classification_to_subcommand(&Classification::ShowSnapshot { sha: "abc1234" }),
ShimSubcommand::Show
);
assert_eq!(
classification_to_subcommand(&Classification::BlameSnapshot {
path: "src/main.rs"
}),
ShimSubcommand::Blame
);
assert_eq!(
classification_to_subcommand(&Classification::GhPrDiff { pr_number: "42" }),
ShimSubcommand::Diff
);
assert_eq!(
classification_to_subcommand(&Classification::Passthrough),
ShimSubcommand::Other
);
}
}