diffctx 1.18.0__tar.gz → 1.18.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {diffctx-1.18.0 → diffctx-1.18.1}/CHANGELOG.md +46 -1
- {diffctx-1.18.0 → diffctx-1.18.1}/Cargo.lock +1 -1
- {diffctx-1.18.0 → diffctx-1.18.1}/PKG-INFO +2 -2
- {diffctx-1.18.0 → diffctx-1.18.1}/README.md +1 -1
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/Cargo.toml +1 -1
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/README.md +1 -1
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +13 -4
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/hook.rs +185 -26
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/impact.rs +296 -66
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/locate.rs +26 -28
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/pybridge.rs +10 -3
- {diffctx-1.18.0 → diffctx-1.18.1}/pyproject.toml +1 -1
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/cli.py +4 -3
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/mcp/server.py +28 -13
- diffctx-1.18.1/src/diffctx/version.py +1 -0
- diffctx-1.18.0/src/diffctx/version.py +0 -1
- {diffctx-1.18.0 → diffctx-1.18.1}/Cargo.toml +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/LICENSE +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/analytics.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/candidate_files.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/change_class.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/analytics.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/bm25.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/budget.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/category_weights.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/edge_weights.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/extensions.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/filtering.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/git.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/graph_filtering.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/importance.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/limits.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/mod.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/mode.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/needs.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/parsers.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/render.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/scoring.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/selection.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/tokenization.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/weights.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/core.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/discovery.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/base.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/config_edges/generic.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/mod.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/bazel.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/c_family.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/dotnet.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/go.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/javascript.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/jvm.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/perl.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/python.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/shell.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/tags.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/terraform.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/similarity/lexical.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/structural/containment.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/structural/sibling.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/structural/testing.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/effective_config.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/excerpt.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/facts/mod.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/facts/scala.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/filtering.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/fragmentation.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/git.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/graph.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/graph_export.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/in_memory_harness.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/interval.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/languages.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/lib.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/main.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/mode.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/parsers/config_parser.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/parsers/generic.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/parsers/mod.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/paths.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/peak_rss.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/pipeline.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/postpass.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/ppr.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/project_graph.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/provenance.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/render.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/resource.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/run_provenance.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/sanitize.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/scoring.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/select.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/signatures.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/stopwords/code.txt +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/stopwords/docs.txt +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/stopwords.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/test_rng.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/testfiles.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/token_corpus.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/tokenizer.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/types.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/utility/importance.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/utility/mod.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/utility/needs.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/crates/diffctx-native/src/utility/objective.rs +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/rust-toolchain.toml +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/__init__.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/__main__.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/_app.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/_diffctx.pyi +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/_native/__init__.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/_native/graph_analytics.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/_native/graph_export.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/_native/pipeline.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/_native/project_graph.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/clipboard.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/ignore.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/logger.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/mcp/__init__.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/mcp/__main__.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/mcp/fetch.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/mcp/legacy.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/mcp/security.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/py.typed +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/tokens.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/tree.py +0 -0
- {diffctx-1.18.0 → diffctx-1.18.1}/src/diffctx/writer.py +0 -0
|
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.18.1] - 2026-09-30
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **The plugin's impact hook never ran under macOS `/bin/bash` 3.2**: an empty
|
|
15
|
+
`gate=()` expanded under `set -u` is an unbound variable there, the pipeline
|
|
16
|
+
died right of the pipe and `|| true` hid it. One never-empty argument array.
|
|
17
|
+
- Hook command parsing: `git add … && git commit` reviews the working tree
|
|
18
|
+
(the add has not run when the hook sees the line — it reviewed the stale
|
|
19
|
+
index and was silent on the commonest agent pattern); `-m a -m b`, `-F file`
|
|
20
|
+
and other value flags no longer count as pathspecs; `git diff A B` and
|
|
21
|
+
`git diff main` are history, not the pending change; `cd dir && git …` and
|
|
22
|
+
a relative `-C` resolve against the agent's cwd.
|
|
23
|
+
- Impact: a test guards a caller only when it names the symbol *and* reaches
|
|
24
|
+
the caller's module (a Rust test mentioning `render` was the guard of a
|
|
25
|
+
Python `render()`, #312); a chunk caller named `main[88]` is looked up as
|
|
26
|
+
`main`; two-letter modules (`db.py`) keep their callers; `*out = f()` and
|
|
27
|
+
`#[derive]` lines are code, not comments; a wide changed-file list folds
|
|
28
|
+
into a count before a single caller is dropped, and a symbol keeps its
|
|
29
|
+
strongest caller; a run that hit a limit says `(partial: …)` and is never
|
|
30
|
+
`empty`; schema contracts are listed before public symbols with a `+N more`
|
|
31
|
+
count; SHA-256 repositories count commits.
|
|
32
|
+
- `diffctx.impact.v1` and `diffctx.locate.v1` required every field the
|
|
33
|
+
serializer may omit (`truncated`, `commit_count`, `deleted_files`, …), so no
|
|
34
|
+
real document validated; the schemas now require what is always present,
|
|
35
|
+
and a test validates real impact and locate output against them.
|
|
36
|
+
- MCP: the default `diff_ref` on a clean single-commit repository is the
|
|
37
|
+
working tree, not an error naming a range the caller never passed; the
|
|
38
|
+
`git status` guess runs off the event loop with a 10 s cap and never leaks
|
|
39
|
+
the command line into the response.
|
|
40
|
+
- CLI: `--mode impact -o out.yaml` is refused like `-f yaml` instead of
|
|
41
|
+
writing JSON silently; `--mode` help names the text default.
|
|
42
|
+
- Plugin installer: the download is unpacked beside its target so the final
|
|
43
|
+
`mv` is a rename, never a copy another session could execute half-written.
|
|
44
|
+
- MCP `mode=impact` with `paths` no longer marks the whole change reviewed.
|
|
45
|
+
- Landing page: the four-part iOS viewport contract (`viewport-fit=cover`,
|
|
46
|
+
status-bar style, safe-area insets, taller `html`).
|
|
47
|
+
- Privacy page names the release-binary download the plugin makes.
|
|
48
|
+
- `bitcheck.sh` ranges re-pinned on the rewritten history; the old anchors
|
|
49
|
+
kept the pre-rewrite tree, with third-party tokens in retired benchmark
|
|
50
|
+
diffs, reachable on both forges.
|
|
51
|
+
- Runtime image: one package layer (Sonar `docker:S7031`/`S6595`); PyJWT
|
|
52
|
+
2.15.1 in `uv.lock` (GHSA-w6j9-cwv2-h6wq, transitive).
|
|
53
|
+
|
|
10
54
|
## [1.18.0] - 2026-09-29
|
|
11
55
|
|
|
12
56
|
### Added
|
|
@@ -1672,7 +1716,8 @@ Earlier releases shipped as `treemapper`; see
|
|
|
1672
1716
|
<https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
|
|
1673
1717
|
release notes (`1.0.0` through `1.6.1`).
|
|
1674
1718
|
|
|
1675
|
-
[Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.18.
|
|
1719
|
+
[Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.18.1...HEAD
|
|
1720
|
+
[1.18.1]: https://github.com/nikolay-e/diffctx/compare/v1.18.0...v1.18.1
|
|
1676
1721
|
[1.18.0]: https://github.com/nikolay-e/diffctx/compare/v1.17.0...v1.18.0
|
|
1677
1722
|
[1.17.0]: https://github.com/nikolay-e/diffctx/compare/v1.16.0...v1.17.0
|
|
1678
1723
|
[1.16.0]: https://github.com/nikolay-e/diffctx/compare/v1.15.0...v1.16.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffctx
|
|
3
|
-
Version: 1.18.
|
|
3
|
+
Version: 1.18.1
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -154,7 +154,7 @@ keeps its git meaning.
|
|
|
154
154
|
| `--full` | false | Only the changed files, every fragment, no related-code context |
|
|
155
155
|
| `--timeout` | 300 | Wall-clock deadline in seconds; on expiry the run stops cooperatively and emits a partial artifact whose `coverage` block names the limit (exit 0). 124 is the watchdog behind it, 30 s later, for a phase that could not stop |
|
|
156
156
|
| `--with-raw-diff` | false | Also embed git's raw unified diff ahead of the selected fragments — additive (selection unchanged), not charged to `--budget`, lock/ignored/secret-like sections omitted. Python CLI only |
|
|
157
|
-
| `--mode` | `pack` | `locate` emits the same ranked selection as compact `diffctx.locate.v1` JSON — path, lines, score, provenance reasons, a blast-radius `summary` and per-item impact `group` (`test`/`type`/`config`), NO source bodies. Adds a `coverage` block naming what the run could not see (`unparsed_files`, `zero_edge_files`, `ppr_truncated`, `next_up`, a heuristic `confidence`) and an `overflow` ranking of what the budget left behind — omitted entirely when there is nothing to disclose. `
|
|
157
|
+
| `--mode` | `pack` | `locate` emits the same ranked selection as compact `diffctx.locate.v1` JSON — path, lines, score, provenance reasons, a blast-radius `summary` and per-item impact `group` (`test`/`type`/`config`), NO source bodies. Adds a `coverage` block naming what the run could not see (`unparsed_files`, `zero_edge_files`, `ppr_truncated`, `next_up`, a heuristic `confidence`) and an `overflow` ranking of what the budget left behind — omitted entirely when there is nothing to disclose. `impact` answers, under 2k tokens, what the change reaches outside its diff: callers in files the diff does not touch, whether a test guards each, cross-commit overlap, and the public API and schema changes (text by default, `diffctx.impact.v1` JSON with `-f json`). The MCP tool takes both as `mode` |
|
|
158
158
|
|
|
159
159
|
Every JSON/YAML artifact opens with `schema: diffctx.context.v1` and validates
|
|
160
160
|
against [`schemas/diffctx.context.v1.json`](schemas/diffctx.context.v1.json),
|
|
@@ -95,7 +95,7 @@ keeps its git meaning.
|
|
|
95
95
|
| `--full` | false | Only the changed files, every fragment, no related-code context |
|
|
96
96
|
| `--timeout` | 300 | Wall-clock deadline in seconds; on expiry the run stops cooperatively and emits a partial artifact whose `coverage` block names the limit (exit 0). 124 is the watchdog behind it, 30 s later, for a phase that could not stop |
|
|
97
97
|
| `--with-raw-diff` | false | Also embed git's raw unified diff ahead of the selected fragments — additive (selection unchanged), not charged to `--budget`, lock/ignored/secret-like sections omitted. Python CLI only |
|
|
98
|
-
| `--mode` | `pack` | `locate` emits the same ranked selection as compact `diffctx.locate.v1` JSON — path, lines, score, provenance reasons, a blast-radius `summary` and per-item impact `group` (`test`/`type`/`config`), NO source bodies. Adds a `coverage` block naming what the run could not see (`unparsed_files`, `zero_edge_files`, `ppr_truncated`, `next_up`, a heuristic `confidence`) and an `overflow` ranking of what the budget left behind — omitted entirely when there is nothing to disclose. `
|
|
98
|
+
| `--mode` | `pack` | `locate` emits the same ranked selection as compact `diffctx.locate.v1` JSON — path, lines, score, provenance reasons, a blast-radius `summary` and per-item impact `group` (`test`/`type`/`config`), NO source bodies. Adds a `coverage` block naming what the run could not see (`unparsed_files`, `zero_edge_files`, `ppr_truncated`, `next_up`, a heuristic `confidence`) and an `overflow` ranking of what the budget left behind — omitted entirely when there is nothing to disclose. `impact` answers, under 2k tokens, what the change reaches outside its diff: callers in files the diff does not touch, whether a test guards each, cross-commit overlap, and the public API and schema changes (text by default, `diffctx.impact.v1` JSON with `-f json`). The MCP tool takes both as `mode` |
|
|
99
99
|
|
|
100
100
|
Every JSON/YAML artifact opens with `schema: diffctx.context.v1` and validates
|
|
101
101
|
against [`schemas/diffctx.context.v1.json`](schemas/diffctx.context.v1.json),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "diffctx"
|
|
3
|
-
version = "1.18.
|
|
3
|
+
version = "1.18.1"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.85"
|
|
6
6
|
description = "Selects the minimum code an LLM needs to review a git diff: walks the dependency graph outward from changed lines and stops when extra context stops paying for itself"
|
|
@@ -95,7 +95,7 @@ keeps its git meaning.
|
|
|
95
95
|
| `--full` | false | Only the changed files, every fragment, no related-code context |
|
|
96
96
|
| `--timeout` | 300 | Wall-clock deadline in seconds; on expiry the run stops cooperatively and emits a partial artifact whose `coverage` block names the limit (exit 0). 124 is the watchdog behind it, 30 s later, for a phase that could not stop |
|
|
97
97
|
| `--with-raw-diff` | false | Also embed git's raw unified diff ahead of the selected fragments — additive (selection unchanged), not charged to `--budget`, lock/ignored/secret-like sections omitted. Python CLI only |
|
|
98
|
-
| `--mode` | `pack` | `locate` emits the same ranked selection as compact `diffctx.locate.v1` JSON — path, lines, score, provenance reasons, a blast-radius `summary` and per-item impact `group` (`test`/`type`/`config`), NO source bodies. Adds a `coverage` block naming what the run could not see (`unparsed_files`, `zero_edge_files`, `ppr_truncated`, `next_up`, a heuristic `confidence`) and an `overflow` ranking of what the budget left behind — omitted entirely when there is nothing to disclose. `
|
|
98
|
+
| `--mode` | `pack` | `locate` emits the same ranked selection as compact `diffctx.locate.v1` JSON — path, lines, score, provenance reasons, a blast-radius `summary` and per-item impact `group` (`test`/`type`/`config`), NO source bodies. Adds a `coverage` block naming what the run could not see (`unparsed_files`, `zero_edge_files`, `ppr_truncated`, `next_up`, a heuristic `confidence`) and an `overflow` ranking of what the budget left behind — omitted entirely when there is nothing to disclose. `impact` answers, under 2k tokens, what the change reaches outside its diff: callers in files the diff does not touch, whether a test guards each, cross-commit overlap, and the public API and schema changes (text by default, `diffctx.impact.v1` JSON with `-f json`). The MCP tool takes both as `mode` |
|
|
99
99
|
|
|
100
100
|
Every JSON/YAML artifact opens with `schema: diffctx.context.v1` and validates
|
|
101
101
|
against [`schemas/diffctx.context.v1.json`](schemas/diffctx.context.v1.json),
|
|
@@ -32,7 +32,10 @@ static FN_DEF_RE: Lazy<Regex> = Lazy::new(|| {
|
|
|
32
32
|
});
|
|
33
33
|
static TYPE_REF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w*)\b").unwrap());
|
|
34
34
|
static FN_CALL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([a-z_]\w+)\s*[(<]").unwrap());
|
|
35
|
-
|
|
35
|
+
/// Whole `a::b::c` paths; every adjacent pair is a (module, symbol) call so
|
|
36
|
+
/// `crate::pricing::total(..)` names `pricing::total` and not only
|
|
37
|
+
/// `crate::pricing`, which a pairwise regex stopped at.
|
|
38
|
+
static PATH_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b\w+(?:::\w+)+").unwrap());
|
|
36
39
|
static IMPL_RE: Lazy<Regex> =
|
|
37
40
|
Lazy::new(|| Regex::new(r"(?m)^\s*impl(?:<[^>]*>)?\s+(\w+)\s+for\s+(\w+)").unwrap());
|
|
38
41
|
static PUB_USE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*pub\s+use\s+([\w:]+)").unwrap());
|
|
@@ -107,9 +110,15 @@ fn extract_references(content: &str) -> References {
|
|
|
107
110
|
fn_calls.insert(name.to_string());
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
|
-
let path_calls: FxHashSet<(String, String)> =
|
|
111
|
-
.
|
|
112
|
-
.
|
|
113
|
+
let path_calls: FxHashSet<(String, String)> = PATH_RE
|
|
114
|
+
.find_iter(content)
|
|
115
|
+
.flat_map(|m| {
|
|
116
|
+
let segments: Vec<&str> = m.as_str().split("::").collect();
|
|
117
|
+
segments
|
|
118
|
+
.windows(2)
|
|
119
|
+
.map(|w| (w[0].to_string(), w[1].to_string()))
|
|
120
|
+
.collect::<Vec<_>>()
|
|
121
|
+
})
|
|
113
122
|
.collect();
|
|
114
123
|
References {
|
|
115
124
|
type_refs,
|
|
@@ -22,7 +22,7 @@ use std::time::{Duration, Instant, SystemTime};
|
|
|
22
22
|
use crate::config::limits::{DEFAULT_PPR_ALPHA, DEFAULT_SCORING};
|
|
23
23
|
use crate::mode::ScoringMode;
|
|
24
24
|
|
|
25
|
-
///
|
|
25
|
+
/// Characters of context handed to the model; the impact renderer already caps
|
|
26
26
|
/// itself in tokens, this is the belt for the belt.
|
|
27
27
|
const MAX_CONTEXT_CHARS: usize = 9_000;
|
|
28
28
|
|
|
@@ -110,7 +110,7 @@ fn words(command: &str) -> Vec<String> {
|
|
|
110
110
|
out.push(std::mem::take(&mut cur));
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
-
|
|
113
|
+
c if OPERATORS.contains(&c) => {
|
|
114
114
|
if !cur.is_empty() {
|
|
115
115
|
out.push(std::mem::take(&mut cur));
|
|
116
116
|
}
|
|
@@ -126,30 +126,91 @@ fn words(command: &str) -> Vec<String> {
|
|
|
126
126
|
out
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
const OPERATORS: &[char] = &[';', '|', '&'];
|
|
130
|
+
|
|
129
131
|
fn is_operator(w: &str) -> bool {
|
|
130
|
-
|
|
132
|
+
w.chars().count() == 1 && w.chars().all(|c| OPERATORS.contains(&c))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Flags whose next word is a value, not a pathspec or a revision.
|
|
136
|
+
const VALUE_FLAGS: &[&str] = &[
|
|
137
|
+
"-m",
|
|
138
|
+
"-F",
|
|
139
|
+
"-C",
|
|
140
|
+
"-c",
|
|
141
|
+
"-t",
|
|
142
|
+
"-S",
|
|
143
|
+
"-G",
|
|
144
|
+
"-O",
|
|
145
|
+
"--message",
|
|
146
|
+
"--file",
|
|
147
|
+
"--author",
|
|
148
|
+
"--date",
|
|
149
|
+
"--fixup",
|
|
150
|
+
"--squash",
|
|
151
|
+
"--template",
|
|
152
|
+
"--reuse-message",
|
|
153
|
+
"--reedit-message",
|
|
154
|
+
"--trailer",
|
|
155
|
+
"--cleanup",
|
|
156
|
+
"--pathspec-from-file",
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
/// The positional arguments before `--`, with the values of value-taking
|
|
160
|
+
/// flags skipped: `git commit -m a -m b` has none, `git commit -m x a.py`
|
|
161
|
+
/// has one.
|
|
162
|
+
fn positionals(args: &[String]) -> Vec<&str> {
|
|
163
|
+
let mut out = Vec::new();
|
|
164
|
+
let mut skip = false;
|
|
165
|
+
for a in args {
|
|
166
|
+
if skip {
|
|
167
|
+
skip = false;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if a == "--" {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
if VALUE_FLAGS.contains(&a.as_str()) {
|
|
174
|
+
skip = true;
|
|
175
|
+
} else if !a.starts_with('-') {
|
|
176
|
+
out.push(a.as_str());
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
out
|
|
131
180
|
}
|
|
132
181
|
|
|
133
|
-
/// Every git or gh statement of the command line: verb, `-C
|
|
134
|
-
///
|
|
135
|
-
/// is two statements, and the second is
|
|
182
|
+
/// Every git or gh statement of the command line: verb, directory (`-C`, or
|
|
183
|
+
/// a `cd` earlier on the line) and the arguments up to the next shell
|
|
184
|
+
/// operator. `git add -A && git commit` is two statements, and the second is
|
|
185
|
+
/// the one that matters.
|
|
136
186
|
fn statements(command: &str) -> Vec<(String, Option<String>, Vec<String>)> {
|
|
137
187
|
let ws = words(command);
|
|
138
188
|
let mut found = Vec::new();
|
|
189
|
+
let mut cwd: Option<String> = None;
|
|
139
190
|
let mut i = 0;
|
|
140
191
|
while i < ws.len() {
|
|
141
192
|
let prog = Path::new(&ws[i])
|
|
142
193
|
.file_name()
|
|
143
194
|
.map(|f| f.to_string_lossy().to_string())
|
|
144
195
|
.unwrap_or_default();
|
|
196
|
+
if ws[i] == "cd" {
|
|
197
|
+
if let Some(target) = ws
|
|
198
|
+
.get(i + 1)
|
|
199
|
+
.filter(|w| !is_operator(w) && !w.starts_with('-'))
|
|
200
|
+
{
|
|
201
|
+
cwd = Some(join_dir(cwd.as_deref(), target));
|
|
202
|
+
}
|
|
203
|
+
i += 1;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
145
206
|
if prog == "git" || prog == "gh" {
|
|
146
207
|
let mut j = i + 1;
|
|
147
|
-
let mut dir =
|
|
208
|
+
let mut dir = cwd.clone();
|
|
148
209
|
let mut verb = None;
|
|
149
210
|
while j < ws.len() && !is_operator(&ws[j]) {
|
|
150
211
|
let w = &ws[j];
|
|
151
212
|
if prog == "git" && w == "-C" {
|
|
152
|
-
dir = ws.get(j + 1).
|
|
213
|
+
dir = ws.get(j + 1).map(|d| join_dir(cwd.as_deref(), d));
|
|
153
214
|
j += 2;
|
|
154
215
|
continue;
|
|
155
216
|
}
|
|
@@ -189,10 +250,38 @@ fn statements(command: &str) -> Vec<(String, Option<String>, Vec<String>)> {
|
|
|
189
250
|
found
|
|
190
251
|
}
|
|
191
252
|
|
|
253
|
+
/// A directory as the shell line spelled it: `/r` is absolute on every
|
|
254
|
+
/// platform here, and the join keeps the line's own separator.
|
|
255
|
+
fn is_absolute_dir(dir: &str) -> bool {
|
|
256
|
+
dir.starts_with('/') || Path::new(dir).is_absolute()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fn join_dir(base: Option<&str>, dir: &str) -> String {
|
|
260
|
+
match base {
|
|
261
|
+
Some(b) if !is_absolute_dir(dir) => format!("{}/{dir}", b.trim_end_matches('/')),
|
|
262
|
+
_ => dir.to_string(),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
192
266
|
pub fn detect(event: Event, command: &str) -> Option<Detected> {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
267
|
+
// The hook runs before the line does: a `git add` earlier on it has not
|
|
268
|
+
// staged anything yet, so the commit that follows records the working
|
|
269
|
+
// tree, not the index this process can see.
|
|
270
|
+
let mut staged_ahead = false;
|
|
271
|
+
for (verb, dir, args) in statements(command) {
|
|
272
|
+
if matches!(verb.as_str(), "add" | "rm" | "mv") {
|
|
273
|
+
staged_ahead = true;
|
|
274
|
+
}
|
|
275
|
+
if let Some(mut detected) = detect_statement(event, &verb, dir, args) {
|
|
276
|
+
if staged_ahead {
|
|
277
|
+
if let Trigger::Commit { all } = &mut detected.trigger {
|
|
278
|
+
*all = true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return Some(detected);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
None
|
|
196
285
|
}
|
|
197
286
|
|
|
198
287
|
fn detect_statement(
|
|
@@ -211,10 +300,7 @@ fn detect_statement(
|
|
|
211
300
|
let short_a = args
|
|
212
301
|
.iter()
|
|
213
302
|
.any(|a| a.starts_with('-') && !a.starts_with("--") && a.contains('a'));
|
|
214
|
-
let pathspec = args
|
|
215
|
-
&& !has("-m")
|
|
216
|
-
&& !has("--message")
|
|
217
|
-
|| args.iter().filter(|a| !a.starts_with('-')).count() > 1;
|
|
303
|
+
let pathspec = !positionals(&args).is_empty() || has("--");
|
|
218
304
|
Trigger::Commit {
|
|
219
305
|
all: has("--all") || short_a || pathspec,
|
|
220
306
|
}
|
|
@@ -249,8 +335,14 @@ fn detect_statement(
|
|
|
249
335
|
Trigger::PullRequest
|
|
250
336
|
}
|
|
251
337
|
(Event::PostToolUse, "diff") => {
|
|
252
|
-
// A diff between
|
|
253
|
-
|
|
338
|
+
// A diff between revisions, or against one other than HEAD, is
|
|
339
|
+
// history, not the pending change; a path before `--` is.
|
|
340
|
+
let revs = positionals(&args);
|
|
341
|
+
let looks_like_path = |r: &str| r.contains('/') || r.starts_with('.');
|
|
342
|
+
if revs.len() > 1
|
|
343
|
+
|| revs.iter().any(|r| r.contains(".."))
|
|
344
|
+
|| revs.iter().any(|r| *r != "HEAD" && !looks_like_path(r))
|
|
345
|
+
{
|
|
254
346
|
return None;
|
|
255
347
|
}
|
|
256
348
|
Trigger::Inspect {
|
|
@@ -336,6 +428,22 @@ pub fn content_key(root: &Path, range: &str) -> Option<String> {
|
|
|
336
428
|
(key.len() == 40).then_some(key)
|
|
337
429
|
}
|
|
338
430
|
|
|
431
|
+
/// A stable key for an answer: FNV-1a over the repository root and the
|
|
432
|
+
/// rendered text — no dependency, no collisions that matter at this scale.
|
|
433
|
+
fn text_key(root: &Path, text: &str) -> String {
|
|
434
|
+
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
|
|
435
|
+
for b in root
|
|
436
|
+
.to_string_lossy()
|
|
437
|
+
.bytes()
|
|
438
|
+
.chain([0u8])
|
|
439
|
+
.chain(text.bytes())
|
|
440
|
+
{
|
|
441
|
+
h ^= u64::from(b);
|
|
442
|
+
h = h.wrapping_mul(0x0000_0100_0000_01b3);
|
|
443
|
+
}
|
|
444
|
+
format!("answer-{h:016x}")
|
|
445
|
+
}
|
|
446
|
+
|
|
339
447
|
fn cache_dir() -> Option<PathBuf> {
|
|
340
448
|
let base = std::env::var_os("DIFFCTX_CACHE_DIR")
|
|
341
449
|
.map(PathBuf::from)
|
|
@@ -407,16 +515,16 @@ fn prepare(event: Event, stdin_json: &str) -> Option<Prepared> {
|
|
|
407
515
|
let payload: serde_json::Value = serde_json::from_str(stdin_json).ok()?;
|
|
408
516
|
let command = payload["tool_input"]["command"].as_str()?;
|
|
409
517
|
let detected = detect(event, command)?;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
518
|
+
// A relative directory on the command line is relative to where the
|
|
519
|
+
// agent ran it, which the payload names; this process's cwd is a guess.
|
|
520
|
+
let base = payload["cwd"]
|
|
521
|
+
.as_str()
|
|
413
522
|
.map(PathBuf::from)
|
|
414
|
-
.or_else(|| payload["cwd"].as_str().map(PathBuf::from))
|
|
415
523
|
.or_else(|| std::env::current_dir().ok())?;
|
|
416
|
-
let root =
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
524
|
+
let root = match detected.dir.as_deref() {
|
|
525
|
+
Some(dir) if is_absolute_dir(dir) => PathBuf::from(dir),
|
|
526
|
+
Some(dir) => base.join(dir),
|
|
527
|
+
None => base,
|
|
420
528
|
};
|
|
421
529
|
let root = crate::git::find_toplevel(&root)?;
|
|
422
530
|
let range = range_for(&root, &detected.trigger)?;
|
|
@@ -442,6 +550,16 @@ fn impact_context(event: Event, root: &Path, range: &str) -> Option<String> {
|
|
|
442
550
|
return None;
|
|
443
551
|
}
|
|
444
552
|
let text = truncate_chars(&crate::impact::render_markdown(&output), MAX_CONTEXT_CHARS);
|
|
553
|
+
// The worktree, the index after `git add`, and the commit itself name
|
|
554
|
+
// different diffs of one change, and the header says so; the substance
|
|
555
|
+
// is the symbols and their callers, and a substance already shown this
|
|
556
|
+
// day is not shown again (#314).
|
|
557
|
+
let substance = serde_json::to_string(&output.changed).unwrap_or_default();
|
|
558
|
+
let answer_key = text_key(root, &substance);
|
|
559
|
+
if is_seen(&answer_key) {
|
|
560
|
+
return None;
|
|
561
|
+
}
|
|
562
|
+
mark_seen(&answer_key);
|
|
445
563
|
let lead = match event {
|
|
446
564
|
Event::PreToolUse => "diffctx reviewed the change this command is about to record.",
|
|
447
565
|
Event::PostToolUse => "diffctx reviewed the pending change you just inspected.",
|
|
@@ -554,10 +672,37 @@ mod tests {
|
|
|
554
672
|
assert_eq!(pre("gh pr create --fill"), Some(Trigger::PullRequest));
|
|
555
673
|
assert!(pre("gh pr view 12").is_none());
|
|
556
674
|
assert!(pre("git status && git log").is_none());
|
|
675
|
+
// The add has not run when the hook sees the line: the commit
|
|
676
|
+
// records the working tree, not the index of this moment.
|
|
557
677
|
assert_eq!(
|
|
558
678
|
pre("git add -A && git commit -m x && git push"),
|
|
679
|
+
Some(Trigger::Commit { all: true })
|
|
680
|
+
);
|
|
681
|
+
assert_eq!(
|
|
682
|
+
pre("git commit -m a -m b"),
|
|
683
|
+
Some(Trigger::Commit { all: false })
|
|
684
|
+
);
|
|
685
|
+
assert_eq!(
|
|
686
|
+
pre("git commit -F msg.txt"),
|
|
559
687
|
Some(Trigger::Commit { all: false })
|
|
560
688
|
);
|
|
689
|
+
assert_eq!(
|
|
690
|
+
pre("git commit -m x src/a.py"),
|
|
691
|
+
Some(Trigger::Commit { all: true })
|
|
692
|
+
);
|
|
693
|
+
assert_eq!(
|
|
694
|
+
detect(Event::PreToolUse, "cd sub/app && git commit -m x").unwrap(),
|
|
695
|
+
Detected {
|
|
696
|
+
trigger: Trigger::Commit { all: false },
|
|
697
|
+
dir: Some("sub/app".to_string())
|
|
698
|
+
}
|
|
699
|
+
);
|
|
700
|
+
assert_eq!(
|
|
701
|
+
detect(Event::PreToolUse, "cd /r && git -C sub commit -m x")
|
|
702
|
+
.unwrap()
|
|
703
|
+
.dir,
|
|
704
|
+
Some("/r/sub".to_string())
|
|
705
|
+
);
|
|
561
706
|
}
|
|
562
707
|
|
|
563
708
|
#[test]
|
|
@@ -572,6 +717,20 @@ mod tests {
|
|
|
572
717
|
Some(Trigger::Inspect { staged: false })
|
|
573
718
|
);
|
|
574
719
|
assert!(post("git diff HEAD~3..HEAD").is_none());
|
|
720
|
+
assert!(post("git diff HEAD~1 HEAD").is_none());
|
|
721
|
+
assert!(post("git diff main").is_none());
|
|
722
|
+
assert_eq!(
|
|
723
|
+
post("git diff HEAD"),
|
|
724
|
+
Some(Trigger::Inspect { staged: false })
|
|
725
|
+
);
|
|
726
|
+
assert_eq!(
|
|
727
|
+
post("git diff -- src/"),
|
|
728
|
+
Some(Trigger::Inspect { staged: false })
|
|
729
|
+
);
|
|
730
|
+
assert_eq!(
|
|
731
|
+
post("git diff src/a.py"),
|
|
732
|
+
Some(Trigger::Inspect { staged: false })
|
|
733
|
+
);
|
|
575
734
|
assert!(post("git commit -m x").is_none());
|
|
576
735
|
assert!(pre("git diff").is_none());
|
|
577
736
|
}
|
|
@@ -580,7 +739,7 @@ mod tests {
|
|
|
580
739
|
fn the_first_statement_wins_and_operators_end_it() {
|
|
581
740
|
assert_eq!(
|
|
582
741
|
pre("git add -A; git commit -m 'a; b' | cat"),
|
|
583
|
-
Some(Trigger::Commit { all:
|
|
742
|
+
Some(Trigger::Commit { all: true })
|
|
584
743
|
);
|
|
585
744
|
assert!(pre("git log | grep commit").is_none());
|
|
586
745
|
}
|