|
8 | 8 | //! agent already walks, with nothing new to decide. After a text search |
9 | 9 | //! for a name the pending change edits, that name's callers. |
10 | 10 | //! |
11 | | -//! Each reads the event on stdin and answers with `additionalContext`, or |
12 | | -//! with nothing when the impact is empty, the same content was already |
13 | | -//! reviewed, or anything at all goes wrong. A hook that blocks a commit |
| 11 | +//! Each reads the event on stdin and answers with `additionalContext`: the |
| 12 | +//! impact, or one line before a commit or push when it is empty or the line |
| 13 | +//! could not be read (#464); nothing when the same content was already |
| 14 | +//! reviewed or anything at all goes wrong. A hook that blocks a commit |
14 | 15 | //! because it failed would be worse than no hook; the one deliberate block, |
15 | 16 | //! the opt-in strict gate, denies with the impact attached so the retry |
16 | 17 | //! passes. |
@@ -1656,6 +1657,8 @@ struct Trace { |
1656 | 1657 | verb: Option<&'static str>, |
1657 | 1658 | root: Option<PathBuf>, |
1658 | 1659 | range: Option<String>, |
| 1660 | + /// Why a one-line answer was given in place of a review, for the log. |
| 1661 | + noted: Option<Silence>, |
1659 | 1662 | } |
1660 | 1663 |
|
1661 | 1664 | /// Why the hook stayed silent, as its log line names it; `shown` otherwise. |
@@ -2341,7 +2344,32 @@ fn decide( |
2341 | 2344 | .map(|c| hook_json(event, &c, false)); |
2342 | 2345 | } |
2343 | 2346 | } |
2344 | | - let prepared = prepare(event, &payload, trace)?; |
| 2347 | + let prepared = match prepare(event, &payload, trace) { |
| 2348 | + Ok(p) => p, |
| 2349 | + // A commit or push the hook could not read is said so, so that no |
| 2350 | + // block means the hook did not run, never "reviewed, nothing" (#464). |
| 2351 | + Err(why @ ("unsupported-syntax" | "unresolved-dir")) |
| 2352 | + if event == Event::PreToolUse |
| 2353 | + && payload["tool_input"]["command"] |
| 2354 | + .as_str() |
| 2355 | + .is_some_and(names_a_recording_verb) => |
| 2356 | + { |
| 2357 | + trace.noted = Some(why); |
| 2358 | + let how = if why == "unresolved-dir" { |
| 2359 | + "it changes into a directory the hook cannot resolve" |
| 2360 | + } else { |
| 2361 | + "its git runs inside a subshell, eval or command substitution" |
| 2362 | + }; |
| 2363 | + return Ok(hook_json( |
| 2364 | + event, |
| 2365 | + &format!( |
| 2366 | + "diffctx did not analyse this command: {how}. Run `diffctx <repo> --diff --mode impact` in that repository before going on." |
| 2367 | + ), |
| 2368 | + false, |
| 2369 | + )); |
| 2370 | + } |
| 2371 | + Err(why) => return Err(why), |
| 2372 | + }; |
2345 | 2373 | if event == Event::PreToolUse && is_seen(&prepared.record_key) { |
2346 | 2374 | return Err("seen"); |
2347 | 2375 | } |
@@ -2391,11 +2419,32 @@ fn decide( |
2391 | 2419 | if event == Event::PreToolUse { |
2392 | 2420 | mark_seen(&record_key); |
2393 | 2421 | } |
2394 | | - answer.map(|a| hook_json(event, &a.context, gate && !a.partial)) |
| 2422 | + match answer { |
| 2423 | + // One line, so that no block means the hook did not run (#464). |
| 2424 | + Err("empty") if event == Event::PreToolUse => { |
| 2425 | + trace.noted = Some("empty"); |
| 2426 | + let range = trace.range.as_deref().unwrap_or("this change"); |
| 2427 | + Ok(hook_json( |
| 2428 | + event, |
| 2429 | + &format!("diffctx: {range} reaches nothing outside the diff."), |
| 2430 | + false, |
| 2431 | + )) |
| 2432 | + } |
| 2433 | + answer => answer.map(|a| hook_json(event, &a.context, gate && !a.partial)), |
| 2434 | + } |
2395 | 2435 | } |
2396 | 2436 |
|
2397 | | -/// The log is cut over to `hook.log.1` past this size: a few thousand runs. |
2398 | | -const LOG_MAX_BYTES: u64 = 256 * 1024; |
| 2437 | +/// A line that records or publishes commits: the events whose silence would |
| 2438 | +/// otherwise read as a clean review. |
| 2439 | +fn names_a_recording_verb(command: &str) -> bool { |
| 2440 | + let words = || command.split(|c: char| !(c.is_alphanumeric() || c == '-')); |
| 2441 | + words().any(|w| w == "git") |
| 2442 | + && words().any(|w| matches!(w, "commit" | "push" | "merge" | "cherry-pick")) |
| 2443 | +} |
| 2444 | + |
| 2445 | +/// The log is cut over to `hook.log.1` past this size: tens of thousands of |
| 2446 | +/// runs, so an audit reads a whole session (#464). |
| 2447 | +const LOG_MAX_BYTES: u64 = 4 * 1024 * 1024; |
2399 | 2448 |
|
2400 | 2449 | /// One line per run in `<cache>/diffctx/hook.log` (#338): Claude Code keeps |
2401 | 2450 | /// only hooks that print, so without it a silent hook that decided "nothing |
@@ -2441,12 +2490,11 @@ pub fn respond_within_deadline(event: Event, stdin_json: String, gate: bool) -> |
2441 | 2490 | let hook_run = crate::resource::RunContext::for_run(HOOK_DEADLINE_SECS); |
2442 | 2491 | let _in_hook = hook_run.enter(); |
2443 | 2492 | let answer = decide(event, &stdin_json, gate, started, &mut trace); |
2444 | | - log_outcome( |
2445 | | - event, |
2446 | | - &trace, |
2447 | | - answer.as_ref().err().copied().unwrap_or("shown"), |
2448 | | - started.elapsed(), |
2449 | | - ); |
| 2493 | + let outcome = match &answer { |
| 2494 | + Ok(_) => trace.noted.unwrap_or("shown"), |
| 2495 | + Err(why) => why, |
| 2496 | + }; |
| 2497 | + log_outcome(event, &trace, outcome, started.elapsed()); |
2450 | 2498 | answer.ok() |
2451 | 2499 | } |
2452 | 2500 |
|
|
0 commit comments