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 std::time::Duration;
use crate::agent_detection::EnvSource;
use crate::metrics::{ShimOutcome, ShimSubcommand};
use crate::shim::classify::{Classification, classify};
use crate::shim::real_git::RealGitExec;
use crate::telemetry::TelemetryGuard;
const PASSTHROUGH_FLUSH_TIMEOUT: Duration = Duration::from_millis(300);
pub(crate) const STRUCTURED_FLUSH_TIMEOUT: Duration = Duration::from_millis(500);
pub(crate) fn passthrough_opt_out_requested(env: &dyn EnvSource) -> bool {
for key in &["GIT_PRISM_PASSTHROUGH", "GIT_PRISM_DISABLE"] {
if let Some(val) = env.get(key) {
let lower = val.to_ascii_lowercase();
if lower == "1" || lower == "true" {
return true;
}
}
}
false
}
pub(crate) fn run_shim<E: EnvSource, G: RealGitExec>(
argv: &[&str],
env: &E,
exec: &G,
telemetry_guard: &mut TelemetryGuard,
) -> ExitCode {
let metrics = crate::metrics::get();
if passthrough_opt_out_requested(env) {
return exec.passthrough(argv);
}
if env.get("GIT_PRISM_INSIDE_SHIM").is_some() {
metrics.record_shim_invocation(ShimOutcome::LoopBreak);
telemetry_guard.force_flush_bounded(PASSTHROUGH_FLUSH_TIMEOUT);
return exec.passthrough(argv);
}
if crate::agent_detection::detect_calling_agent(env).is_none() {
metrics.record_shim_invocation(ShimOutcome::NoAgent);
telemetry_guard.force_flush_bounded(PASSTHROUGH_FLUSH_TIMEOUT);
return exec.passthrough(argv);
}
let classification = classify(argv);
let subcommand = classification_to_subcommand(&classification);
if classification == Classification::Passthrough {
metrics.record_shim_invocation(ShimOutcome::Passthrough);
telemetry_guard.force_flush_bounded(PASSTHROUGH_FLUSH_TIMEOUT);
return exec.passthrough(argv);
}
let repo_path = match resolve_repo_path(env) {
Some(p) => p,
None => {
metrics.record_shim_invocation(ShimOutcome::Passthrough);
telemetry_guard.force_flush_bounded(PASSTHROUGH_FLUSH_TIMEOUT);
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::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 opt_out_recognizes_passthrough_equals_one() {
let env = MapEnv(HashMap::from([("GIT_PRISM_PASSTHROUGH", "1")]));
assert!(passthrough_opt_out_requested(&env));
}
#[test]
fn opt_out_recognizes_passthrough_equals_true_case_insensitive() {
let env = MapEnv(HashMap::from([("GIT_PRISM_PASSTHROUGH", "TRUE")]));
assert!(passthrough_opt_out_requested(&env));
}
#[test]
fn opt_out_recognizes_disable_alias() {
let env = MapEnv(HashMap::from([("GIT_PRISM_DISABLE", "1")]));
assert!(passthrough_opt_out_requested(&env));
}
#[test]
fn opt_out_inactive_when_var_is_zero() {
let env = MapEnv(HashMap::from([("GIT_PRISM_PASSTHROUGH", "0")]));
assert!(!passthrough_opt_out_requested(&env));
}
#[test]
fn opt_out_inactive_when_var_is_false() {
let env = MapEnv(HashMap::from([("GIT_PRISM_PASSTHROUGH", "false")]));
assert!(!passthrough_opt_out_requested(&env));
}
#[test]
fn opt_out_inactive_when_var_is_empty() {
let env = MapEnv(HashMap::from([("GIT_PRISM_PASSTHROUGH", "")]));
assert!(!passthrough_opt_out_requested(&env));
}
#[test]
fn opt_out_inactive_when_var_is_unset() {
let env = MapEnv(HashMap::new());
assert!(!passthrough_opt_out_requested(&env));
}
#[test]
fn it_passes_through_immediately_when_passthrough_env_var_is_set() {
let env = MapEnv(HashMap::from([
("CLAUDECODE", "1"),
("GIT_PRISM_PASSTHROUGH", "1"),
]));
let exec = SpyExec::new(ExitCode::SUCCESS);
let mut guard = crate::telemetry::TelemetryGuard::noop();
let code = run_shim(&["git", "diff", "HEAD~1..HEAD"], &env, &exec, &mut guard);
assert!(
exec.called.get(),
"expected immediate passthrough when GIT_PRISM_PASSTHROUGH=1"
);
assert_eq!(code, ExitCode::SUCCESS);
}
#[test]
fn it_passes_through_immediately_when_disable_alias_is_set() {
let env = MapEnv(HashMap::from([
("CLAUDECODE", "1"),
("GIT_PRISM_DISABLE", "1"),
]));
let exec = SpyExec::new(ExitCode::SUCCESS);
let mut guard = crate::telemetry::TelemetryGuard::noop();
run_shim(&["git", "diff", "HEAD~1..HEAD"], &env, &exec, &mut guard);
assert!(
exec.called.get(),
"expected immediate passthrough when GIT_PRISM_DISABLE=1"
);
}
#[test]
fn it_propagates_nonzero_exit_code_on_passthrough_opt_out() {
let env = MapEnv(HashMap::from([
("CLAUDECODE", "1"),
("GIT_PRISM_PASSTHROUGH", "1"),
]));
let exec = SpyExec::new(ExitCode::from(2));
let mut guard = crate::telemetry::TelemetryGuard::noop();
let code = run_shim(&["git", "diff", "HEAD~1..HEAD"], &env, &exec, &mut guard);
assert!(
exec.called.get(),
"expected immediate passthrough when GIT_PRISM_PASSTHROUGH=1"
);
assert_eq!(code, ExitCode::from(2));
}
#[test]
fn it_does_not_passthrough_immediately_when_opt_out_var_is_unset() {
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 mut guard = crate::telemetry::TelemetryGuard::noop();
run_shim(&["git", "diff", "HEAD~1..HEAD"], &env, &exec, &mut guard);
assert!(
!exec.called.get(),
"without opt-out, a classified agent command must dispatch to the handler, not passthrough"
);
}
#[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);
let mut guard = crate::telemetry::TelemetryGuard::noop();
run_shim(&["git", "diff", "main..HEAD"], &env, &exec, &mut guard);
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);
let mut guard = crate::telemetry::TelemetryGuard::noop();
run_shim(&["git", "diff", "main..HEAD"], &env, &exec, &mut guard);
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);
let mut guard = crate::telemetry::TelemetryGuard::noop();
run_shim(&["git", "status"], &env, &exec, &mut guard);
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);
let mut guard = crate::telemetry::TelemetryGuard::noop();
run_shim(&["git", "diff", "main..HEAD"], &env, &exec, &mut guard);
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 mut guard = crate::telemetry::TelemetryGuard::noop();
let code = run_shim(&["git", "diff", "HEAD~1..HEAD"], &env, &exec, &mut guard);
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);
let mut guard = crate::telemetry::TelemetryGuard::noop();
run_shim(&["git", "diff", "main..HEAD"], &env, &exec, &mut guard);
assert!(
exec.called.get(),
"expected passthrough when current directory cannot be determined"
);
}
#[test]
fn structured_flush_timeout_differs_from_passthrough_flush_timeout() {
assert_ne!(
STRUCTURED_FLUSH_TIMEOUT, PASSTHROUGH_FLUSH_TIMEOUT,
"structured and passthrough flush timeouts must be different values; \
the structured path can afford a longer cap"
);
assert!(
STRUCTURED_FLUSH_TIMEOUT > PASSTHROUGH_FLUSH_TIMEOUT,
"structured-path timeout should be >= passthrough timeout \
since the agent already has its response"
);
}
#[test]
fn structured_flush_timeout_constant_is_positive_and_below_export_timeout() {
assert!(
STRUCTURED_FLUSH_TIMEOUT.as_millis() > 0,
"STRUCTURED_FLUSH_TIMEOUT must be positive"
);
assert!(
STRUCTURED_FLUSH_TIMEOUT.as_secs() < 5,
"STRUCTURED_FLUSH_TIMEOUT must be below the 5 s SDK export timeout"
);
}
#[tokio::test]
async fn structured_path_bounded_flush_returns_within_deadline_on_black_hole_endpoint() {
use std::sync::Mutex;
static ENV_MUTEX: Mutex<()> = Mutex::new(());
let _lock = ENV_MUTEX.lock().unwrap();
unsafe {
std::env::set_var("GIT_PRISM_OTLP_ENDPOINT", "http://192.0.2.1:4318");
}
let mut guard = crate::telemetry::init_quiet();
assert!(
guard.is_active(),
"guard must be active to exercise the structured-path bounded flush"
);
let start = std::time::Instant::now();
guard.force_flush_bounded(STRUCTURED_FLUSH_TIMEOUT);
let elapsed = start.elapsed();
unsafe {
std::env::remove_var("GIT_PRISM_OTLP_ENDPOINT");
}
drop(guard);
assert!(
elapsed < Duration::from_secs(2),
"structured-path bounded flush must not stall significantly beyond the 500ms cap; \
took {elapsed:?}"
);
}
#[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::Passthrough),
ShimSubcommand::Other
);
}
}