diffctx 1.10.1__tar.gz → 1.10.2__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.10.1 → diffctx-1.10.2}/PKG-INFO +1 -1
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/git.rs +129 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/parsers/markdown.rs +54 -13
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/pipeline.rs +42 -2
- {diffctx-1.10.1 → diffctx-1.10.2}/pyproject.toml +1 -1
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/cli.py +26 -5
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/ignore.py +16 -3
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/main.py +31 -10
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/mcp/__main__.py +7 -0
- diffctx-1.10.2/src/diffctx/version.py +1 -0
- diffctx-1.10.1/src/diffctx/version.py +0 -1
- {diffctx-1.10.1 → diffctx-1.10.2}/CHANGELOG.md +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/LICENSE +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/README.md +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/Cargo.lock +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/Cargo.toml +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/analytics.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/candidate_files.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/analytics.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/bm25.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/budget.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/category_weights.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/edge_weights.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/env_overrides.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/extensions.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/filtering.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/fragmentation.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/git.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/graph_filtering.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/importance.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/limits.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/mod.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/mode.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/needs.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/parsers.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/render.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/scoring.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/selection.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/tokenization.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/config/weights.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/core.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/discovery.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/base.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/config_edges/build_system.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/config_edges/cicd.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/config_edges/docker.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/config_edges/generic.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/config_edges/helm.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/config_edges/kubernetes.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/config_edges/mod.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/document/mod.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/history/cochange.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/history/mod.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/mod.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/ansible.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/bazel.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/c_family.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/cargo_edges.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/clojure.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/css.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/dart.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/dbt.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/dotnet.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/elixir.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/erlang.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/go.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/graphql.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/haskell.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/javascript.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/julia.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/jvm.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/latex.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/lua.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/mod.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/nim.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/nix.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/ocaml.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/openapi.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/perl.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/php.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/prisma.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/protobuf.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/python.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/r_lang.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/ruby.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/rust_lang.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/shell.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/sql.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/swift.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/tags.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/terraform.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/semantic/zig.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/similarity/lexical.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/similarity/mod.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/structural/containment.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/structural/mod.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/structural/sibling.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/edges/structural/testing.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/filtering.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/fragmentation.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/graph.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/graph_export.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/interval.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/languages.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/lib.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/main.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/memory_pipeline.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/mode.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/parsers/config_parser.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/parsers/generic.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/parsers/mod.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/parsers/tree_sitter_strategy.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/postpass.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/ppr.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/project_graph.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/pybridge.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/render.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/scoring.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/select.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/signatures.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/stopwords.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/test_harness.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/tokenizer.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/types.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/utility/boltzmann.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/utility/importance.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/utility/mod.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/utility/needs.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/src/utility/scoring.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/tests/common/mod.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/tests/fixtures/garbage/garbage_api.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/tests/fixtures/garbage/garbage_constants.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/tests/fixtures/garbage/garbage_handlers.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/tests/fixtures/garbage/garbage_models.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/tests/fixtures/garbage/garbage_module.js +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/tests/fixtures/garbage/garbage_services.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/tests/fixtures/garbage/garbage_types.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/tests/fixtures/garbage/garbage_unrelated.yaml +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/tests/fixtures/garbage/garbage_utils.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/tests/fixtures/garbage/garbage_validators.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/diffctx/tests/yaml_cases.rs +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/rust-toolchain.toml +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/__init__.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/__main__.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/clipboard.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/diffctx/__init__.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/diffctx/graph_analytics.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/diffctx/graph_export.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/diffctx/pipeline.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/diffctx/project_graph.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/logger.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/mcp/README.md +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/mcp/__init__.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/mcp/formatting.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/mcp/security.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/mcp/server.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/py.typed +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/tokens.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/tree.py +0 -0
- {diffctx-1.10.1 → diffctx-1.10.2}/src/diffctx/writer.py +0 -0
|
@@ -500,6 +500,135 @@ pub fn get_untracked_files(repo_root: &Path) -> Result<Vec<PathBuf>> {
|
|
|
500
500
|
.collect())
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
/// Rewrites one `.diffctx/ignore` pattern line to be anchored to the
|
|
504
|
+
/// directory that contains the `.diffctx/` folder (`rel`, repo-root-relative,
|
|
505
|
+
/// "" for the repo root itself). Mirrors `_process_ignore_line` in the
|
|
506
|
+
/// Python tree-mode ignore resolver (`src/diffctx/ignore.py`) so a pattern
|
|
507
|
+
/// declared in `sub/.diffctx/ignore` only ever matches within `sub/`.
|
|
508
|
+
fn anchor_diffctx_ignore_line(line: &str, rel: &str) -> String {
|
|
509
|
+
let (neg, pat) = match line.strip_prefix('!') {
|
|
510
|
+
Some(rest) => (true, rest),
|
|
511
|
+
None => (false, line),
|
|
512
|
+
};
|
|
513
|
+
let pat_no_trailing_slash = pat.trim_end_matches('/');
|
|
514
|
+
let full = if pat_no_trailing_slash.starts_with('/') || pat_no_trailing_slash.contains('/') {
|
|
515
|
+
let anchored = pat.trim_start_matches('/');
|
|
516
|
+
if rel.is_empty() {
|
|
517
|
+
format!("/{anchored}")
|
|
518
|
+
} else {
|
|
519
|
+
format!("/{rel}/{anchored}")
|
|
520
|
+
}
|
|
521
|
+
} else if rel.is_empty() {
|
|
522
|
+
pat.to_string()
|
|
523
|
+
} else {
|
|
524
|
+
format!("{rel}/**/{pat}")
|
|
525
|
+
};
|
|
526
|
+
if neg { format!("!{full}") } else { full }
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/// Finds every `.diffctx/ignore` file tracked or present in `repo_root`
|
|
530
|
+
/// (any depth) and returns its patterns rewritten to be repo-root-relative,
|
|
531
|
+
/// ready to feed into a combined gitignore-syntax exclude file.
|
|
532
|
+
fn collect_diffctx_ignore_patterns(repo_root: &Path) -> Vec<String> {
|
|
533
|
+
let Ok(files) = run_git_z(
|
|
534
|
+
repo_root,
|
|
535
|
+
&[
|
|
536
|
+
"ls-files",
|
|
537
|
+
"-z",
|
|
538
|
+
"--cached",
|
|
539
|
+
"--others",
|
|
540
|
+
"--exclude-standard",
|
|
541
|
+
"--",
|
|
542
|
+
":(glob)**/.diffctx/ignore",
|
|
543
|
+
],
|
|
544
|
+
) else {
|
|
545
|
+
return Vec::new();
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
let mut patterns = Vec::new();
|
|
549
|
+
for raw in &files {
|
|
550
|
+
let rel_path = unquote_c_style(raw);
|
|
551
|
+
if !rel_path.ends_with(".diffctx/ignore") {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
let rel_dir = rel_path
|
|
555
|
+
.strip_suffix(".diffctx/ignore")
|
|
556
|
+
.unwrap_or("")
|
|
557
|
+
.trim_end_matches('/');
|
|
558
|
+
let Ok(content) = std::fs::read_to_string(repo_root.join(&rel_path)) else {
|
|
559
|
+
continue;
|
|
560
|
+
};
|
|
561
|
+
for line in content.lines() {
|
|
562
|
+
let line = line.trim_end();
|
|
563
|
+
if line.is_empty() || line.starts_with('#') {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
patterns.push(anchor_diffctx_ignore_line(line, rel_dir));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
patterns
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/// Returns the subset of `rel_paths` (repo-root-relative) excluded by either
|
|
573
|
+
/// `.gitignore` (via git's own engine, so nesting/negation/`**` are handled
|
|
574
|
+
/// correctly) or `.diffctx/ignore` (patterns anchored per-directory and fed
|
|
575
|
+
/// to git as a temporary `core.excludesFile`, so the same engine evaluates
|
|
576
|
+
/// both mechanisms uniformly). Best-effort: any failure returns an empty set
|
|
577
|
+
/// rather than blocking the diff pipeline on an ignore-resolution problem.
|
|
578
|
+
pub fn find_ignored_paths(repo_root: &Path, rel_paths: &[String]) -> FxHashSet<String> {
|
|
579
|
+
if rel_paths.is_empty() {
|
|
580
|
+
return FxHashSet::default();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
let diffctx_patterns = collect_diffctx_ignore_patterns(repo_root);
|
|
584
|
+
let temp_excludes = if diffctx_patterns.is_empty() {
|
|
585
|
+
None
|
|
586
|
+
} else {
|
|
587
|
+
let path = std::env::temp_dir().join(format!("diffctx-ignore-{}.tmp", std::process::id()));
|
|
588
|
+
match std::fs::write(&path, diffctx_patterns.join("\n")) {
|
|
589
|
+
Ok(()) => Some(path),
|
|
590
|
+
Err(_) => None,
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
let mut args: Vec<String> = vec!["check-ignore".into(), "--no-index".into()];
|
|
595
|
+
if let Some(ref path) = temp_excludes {
|
|
596
|
+
args.insert(0, format!("core.excludesFile={}", path.display()));
|
|
597
|
+
args.insert(0, "-c".into());
|
|
598
|
+
}
|
|
599
|
+
args.push("--".into());
|
|
600
|
+
args.extend(rel_paths.iter().cloned());
|
|
601
|
+
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
|
|
602
|
+
|
|
603
|
+
let result = (|| -> Result<FxHashSet<String>> {
|
|
604
|
+
let mut cmd = Command::new("git");
|
|
605
|
+
cmd.arg("-C")
|
|
606
|
+
.arg(repo_root)
|
|
607
|
+
.args(&arg_refs)
|
|
608
|
+
.stdout(Stdio::piped())
|
|
609
|
+
.stderr(Stdio::piped());
|
|
610
|
+
let child = cmd.spawn()?;
|
|
611
|
+
let output = wait_with_timeout(child, Duration::from_secs(git_timeout()), &arg_refs)?;
|
|
612
|
+
// Exit code 1 from `check-ignore` means "none of the given paths are
|
|
613
|
+
// ignored" — not a failure. Any other non-zero code is a real error.
|
|
614
|
+
if !output.status.success() && output.status.code() != Some(1) {
|
|
615
|
+
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
616
|
+
return Err(GitError::CommandFailed(format!(
|
|
617
|
+
"git check-ignore failed: {}",
|
|
618
|
+
stderr.trim()
|
|
619
|
+
)));
|
|
620
|
+
}
|
|
621
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
622
|
+
Ok(stdout.lines().map(unquote_c_style).collect())
|
|
623
|
+
})();
|
|
624
|
+
|
|
625
|
+
if let Some(path) = temp_excludes {
|
|
626
|
+
let _ = std::fs::remove_file(path);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
result.unwrap_or_default()
|
|
630
|
+
}
|
|
631
|
+
|
|
503
632
|
pub struct CatFileBatch {
|
|
504
633
|
repo_root: PathBuf,
|
|
505
634
|
child: Option<Child>,
|
|
@@ -54,8 +54,8 @@ impl FragmentationStrategy for MarkdownStrategy {
|
|
|
54
54
|
let total_lines = lines.len() as u32;
|
|
55
55
|
let mut fragments: Vec<Fragment> = Vec::new();
|
|
56
56
|
|
|
57
|
-
for (idx, &(start_line,
|
|
58
|
-
let end_line = find_section_end(&headings, idx,
|
|
57
|
+
for (idx, &(start_line, _level, _)) in headings.iter().enumerate() {
|
|
58
|
+
let end_line = find_section_end(&headings, idx, total_lines);
|
|
59
59
|
if end_line < start_line {
|
|
60
60
|
continue;
|
|
61
61
|
}
|
|
@@ -90,16 +90,57 @@ impl FragmentationStrategy for MarkdownStrategy {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
93
|
+
/// A heading's own fragment ends at the very next heading, regardless of
|
|
94
|
+
/// level — not at the next heading of the *same or higher* level. The old
|
|
95
|
+
/// same-or-higher rule made a heading's span include every nested
|
|
96
|
+
/// subsection beneath it, so a lone top-level `#` heading (the common case:
|
|
97
|
+
/// one H1 title, many `##`/`###` children) spanned the entire rest of the
|
|
98
|
+
/// document. A one-line change in the H1's own preamble then flagged that
|
|
99
|
+
/// all-encompassing fragment as `role: "changed"`, dumping the whole file
|
|
100
|
+
/// instead of just the touched paragraph (#91).
|
|
101
|
+
fn find_section_end(headings: &[(u32, u32, String)], idx: usize, total_lines: u32) -> u32 {
|
|
102
|
+
match headings.get(idx + 1) {
|
|
103
|
+
Some(&(next_line, _, _)) => next_line - 1,
|
|
104
|
+
None => total_lines,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[cfg(test)]
|
|
109
|
+
mod tests {
|
|
110
|
+
use super::*;
|
|
111
|
+
|
|
112
|
+
/// Regression for #91: a lone H1 title followed only by `##` children
|
|
113
|
+
/// must not produce a single fragment spanning the whole document — the
|
|
114
|
+
/// H1's own fragment is just its direct preamble, up to the first child.
|
|
115
|
+
#[test]
|
|
116
|
+
fn h1_fragment_stops_at_first_child_heading_not_eof() {
|
|
117
|
+
let content =
|
|
118
|
+
"# Title\n\nPreamble line.\n\n## Section One\nBody one.\n\n## Section Two\nBody two.\n";
|
|
119
|
+
let fragments = MarkdownStrategy.fragment(Arc::from("doc.md"), content);
|
|
120
|
+
|
|
121
|
+
let title = fragments
|
|
122
|
+
.iter()
|
|
123
|
+
.find(|f| f.content.starts_with("# Title"))
|
|
124
|
+
.expect("Title fragment present");
|
|
125
|
+
assert_eq!(title.id.start_line, 1);
|
|
126
|
+
assert_eq!(
|
|
127
|
+
title.id.end_line, 4,
|
|
128
|
+
"H1 fragment should stop before '## Section One', not run to EOF"
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#[test]
|
|
133
|
+
fn sibling_headings_still_each_get_their_own_fragment() {
|
|
134
|
+
let content =
|
|
135
|
+
"## Section One\nBody one.\n\n## Section Two\nBody two.\n### Nested\nNested body.\n";
|
|
136
|
+
let fragments = MarkdownStrategy.fragment(Arc::from("doc.md"), content);
|
|
137
|
+
assert_eq!(fragments.len(), 3);
|
|
138
|
+
|
|
139
|
+
let nested = fragments
|
|
140
|
+
.iter()
|
|
141
|
+
.find(|f| f.content.starts_with("### Nested"))
|
|
142
|
+
.expect("Nested fragment present");
|
|
143
|
+
assert_eq!(nested.id.start_line, 6);
|
|
144
|
+
assert_eq!(nested.id.end_line, 8);
|
|
103
145
|
}
|
|
104
|
-
total_lines
|
|
105
146
|
}
|
|
@@ -131,6 +131,13 @@ pub fn compute_scored_state(
|
|
|
131
131
|
return Ok(empty_scored_state(root_dir));
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
let ignored_rel_paths = resolve_ignored_paths(&root_dir, &hunks);
|
|
135
|
+
hunks.retain(|h| !is_ignored_path(&root_dir, Path::new(&*h.path), &ignored_rel_paths));
|
|
136
|
+
|
|
137
|
+
if hunks.is_empty() {
|
|
138
|
+
return Ok(empty_scored_state(root_dir));
|
|
139
|
+
}
|
|
140
|
+
|
|
134
141
|
let diff_text = git::get_diff_text(&root_dir, diff_range)?;
|
|
135
142
|
|
|
136
143
|
let mut changed_files = git::get_changed_files(&root_dir, diff_range)?;
|
|
@@ -152,7 +159,9 @@ pub fn compute_scored_state(
|
|
|
152
159
|
.into_iter()
|
|
153
160
|
.filter(|f| {
|
|
154
161
|
let resolved = f.canonicalize().unwrap_or_else(|_| f.clone());
|
|
155
|
-
!excluded.contains(&resolved)
|
|
162
|
+
!excluded.contains(&resolved)
|
|
163
|
+
&& !is_secret_path(f)
|
|
164
|
+
&& !is_ignored_path(&root_dir, f, &ignored_rel_paths)
|
|
156
165
|
})
|
|
157
166
|
.collect();
|
|
158
167
|
|
|
@@ -494,6 +503,31 @@ fn is_secret_path(path: &Path) -> bool {
|
|
|
494
503
|
)
|
|
495
504
|
}
|
|
496
505
|
|
|
506
|
+
fn rel_path_string(root_dir: &Path, path: &Path) -> Option<String> {
|
|
507
|
+
path.strip_prefix(root_dir)
|
|
508
|
+
.ok()
|
|
509
|
+
.map(|p| p.to_string_lossy().replace('\\', "/"))
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/// Resolves `.gitignore` / `.diffctx/ignore` exclusions (#85) for every path
|
|
513
|
+
/// touched by `hunks`, in one batched git call. diff mode previously only
|
|
514
|
+
/// ever excluded a hardcoded set of secret-like filenames (`is_secret_path`)
|
|
515
|
+
/// — a file a user explicitly excluded via `.diffctx/ignore` still had its
|
|
516
|
+
/// changed content surfaced in full.
|
|
517
|
+
fn resolve_ignored_paths(root_dir: &Path, hunks: &[crate::types::DiffHunk]) -> FxHashSet<String> {
|
|
518
|
+
let rel_paths: Vec<String> = hunks
|
|
519
|
+
.iter()
|
|
520
|
+
.filter_map(|h| rel_path_string(root_dir, Path::new(&*h.path)))
|
|
521
|
+
.collect();
|
|
522
|
+
git::find_ignored_paths(root_dir, &rel_paths)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
fn is_ignored_path(root_dir: &Path, path: &Path, ignored_rel_paths: &FxHashSet<String>) -> bool {
|
|
526
|
+
rel_path_string(root_dir, path)
|
|
527
|
+
.map(|rel| ignored_rel_paths.contains(&rel))
|
|
528
|
+
.unwrap_or(false)
|
|
529
|
+
}
|
|
530
|
+
|
|
497
531
|
fn build_diff_context_full(
|
|
498
532
|
root_dir: &Path,
|
|
499
533
|
diff_range: Option<&str>,
|
|
@@ -513,8 +547,14 @@ fn build_diff_context_full(
|
|
|
513
547
|
if hunks.is_empty() {
|
|
514
548
|
return Ok(empty_output(&root_dir));
|
|
515
549
|
}
|
|
550
|
+
let ignored_rel_paths = resolve_ignored_paths(&root_dir, &hunks);
|
|
551
|
+
hunks.retain(|h| !is_ignored_path(&root_dir, Path::new(&*h.path), &ignored_rel_paths));
|
|
552
|
+
if hunks.is_empty() {
|
|
553
|
+
return Ok(empty_output(&root_dir));
|
|
554
|
+
}
|
|
516
555
|
let mut changed_files = git::get_changed_files(&root_dir, diff_range)?;
|
|
517
|
-
changed_files
|
|
556
|
+
changed_files
|
|
557
|
+
.retain(|f| !is_secret_path(f) && !is_ignored_path(&root_dir, f, &ignored_rel_paths));
|
|
518
558
|
if changed_files.is_empty() {
|
|
519
559
|
return Ok(empty_output(&root_dir));
|
|
520
560
|
}
|
|
@@ -24,17 +24,28 @@ class _Unset:
|
|
|
24
24
|
_UNSET: _Unset = _Unset()
|
|
25
25
|
_DIFF_SENTINEL = "__DIFFCTX_DIFF_BARE__"
|
|
26
26
|
|
|
27
|
+
# Set once at the top of parse_args(); read by _exit_error/_warn so every
|
|
28
|
+
# CLI-side validation message is branded like argparse's own native errors
|
|
29
|
+
# (`{prog}: error: ...`) instead of a bare, unbranded "Error: ...". Module
|
|
30
|
+
# state (rather than threading `prog` through every _validate_* signature)
|
|
31
|
+
# keeps this a same-session fix; see #89.
|
|
32
|
+
_current_prog: str = "diffctx"
|
|
33
|
+
|
|
27
34
|
|
|
28
35
|
def _exit_error(message: str) -> NoReturn:
|
|
29
|
-
print(f"
|
|
36
|
+
print(f"{_current_prog}: error: {message}", file=sys.stderr)
|
|
30
37
|
sys.exit(1)
|
|
31
38
|
|
|
32
39
|
|
|
40
|
+
def _warn(message: str) -> None:
|
|
41
|
+
print(f"{_current_prog}: warning: {message}", file=sys.stderr)
|
|
42
|
+
|
|
43
|
+
|
|
33
44
|
def _validate_max_depth(max_depth: int | None) -> None:
|
|
34
45
|
if max_depth is not None and max_depth < 0:
|
|
35
46
|
_exit_error(f"--max-depth must be non-negative, got {max_depth}")
|
|
36
47
|
if max_depth == 0:
|
|
37
|
-
|
|
48
|
+
_warn("--max-depth 0 produces empty tree (root only, no children)")
|
|
38
49
|
|
|
39
50
|
|
|
40
51
|
def _validate_max_file_bytes(max_file_bytes: int, no_file_size_limit: bool) -> int | None:
|
|
@@ -196,6 +207,7 @@ class ParsedArgs:
|
|
|
196
207
|
graph: GraphArgs | None = None
|
|
197
208
|
extra_dirs: list[Path] | None = None
|
|
198
209
|
extra_files: list[Path] | None = None
|
|
210
|
+
no_explicit_paths: bool = False
|
|
199
211
|
|
|
200
212
|
|
|
201
213
|
DEFAULT_IGNORES_HELP = """
|
|
@@ -230,6 +242,8 @@ Examples:
|
|
|
230
242
|
diffctx . --diff HEAD~1 Show context for last commit
|
|
231
243
|
diffctx . -c Copy output to clipboard
|
|
232
244
|
diffctx . --no-content Structure only, no file contents
|
|
245
|
+
diffctx graph . Build the project dependency graph (see: diffctx graph --help)
|
|
246
|
+
diffctx graph . --summary Print graph stats (cycles, hotspots, coupling)
|
|
233
247
|
|
|
234
248
|
Output routing:
|
|
235
249
|
Default: stdout
|
|
@@ -303,7 +317,7 @@ def _build_main_parser(prog: str = "diffctx", version: str = __version__) -> arg
|
|
|
303
317
|
)
|
|
304
318
|
|
|
305
319
|
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {version}")
|
|
306
|
-
parser.add_argument("paths", nargs="*", default=[
|
|
320
|
+
parser.add_argument("paths", nargs="*", default=[], help="Directories, files, or glob patterns to analyze")
|
|
307
321
|
parser.add_argument(
|
|
308
322
|
"-f",
|
|
309
323
|
"--format",
|
|
@@ -360,7 +374,11 @@ def _build_main_parser(prog: str = "diffctx", version: str = __version__) -> arg
|
|
|
360
374
|
type=float,
|
|
361
375
|
default=_UNSET,
|
|
362
376
|
metavar="F",
|
|
363
|
-
help=
|
|
377
|
+
help=(
|
|
378
|
+
"Minimum relevance to include a fragment (default: 0.12, typical range 0-1; "
|
|
379
|
+
"lower = more surrounding context. E.g. --tau 2 excludes nearly everything but "
|
|
380
|
+
"the changed code itself, since only directly-changed fragments score that high)"
|
|
381
|
+
),
|
|
364
382
|
)
|
|
365
383
|
diff_group.add_argument(
|
|
366
384
|
"--scoring",
|
|
@@ -393,7 +411,7 @@ def _warn_diff_only_flags(args: argparse.Namespace) -> None:
|
|
|
393
411
|
used.append("--scoring")
|
|
394
412
|
if used:
|
|
395
413
|
flags = ", ".join(used)
|
|
396
|
-
|
|
414
|
+
_warn(f"diff-mode flags ignored without --diff: {flags}")
|
|
397
415
|
|
|
398
416
|
|
|
399
417
|
def _build_graph_parsed_args(args: argparse.Namespace) -> ParsedArgs:
|
|
@@ -477,10 +495,13 @@ def _build_tree_parsed_args(args: argparse.Namespace) -> ParsedArgs:
|
|
|
477
495
|
full_diff=args.full,
|
|
478
496
|
extra_dirs=extra_dirs,
|
|
479
497
|
extra_files=extra_files,
|
|
498
|
+
no_explicit_paths=not args.paths,
|
|
480
499
|
)
|
|
481
500
|
|
|
482
501
|
|
|
483
502
|
def parse_args(argv: list[str] | None = None, *, prog: str = "diffctx", version: str = __version__) -> ParsedArgs:
|
|
503
|
+
global _current_prog
|
|
504
|
+
_current_prog = prog
|
|
484
505
|
raw_args = sys.argv[1:] if argv is None else argv
|
|
485
506
|
|
|
486
507
|
if raw_args and raw_args[0] == "graph":
|
|
@@ -108,9 +108,17 @@ def _process_ignore_line(line: str, rel: str) -> str:
|
|
|
108
108
|
return ("!" + full) if neg else full
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
_DIFFCTX_IGNORE_RELPATH = f"{DIFFCTX_CONFIG_DIR}/{DIFFCTX_DIR_IGNORE}"
|
|
112
|
+
|
|
113
|
+
|
|
111
114
|
def _collect_from_git(ignore_files: list[Path], root: Path, out: list[str]) -> None:
|
|
112
115
|
for ignore_path in ignore_files:
|
|
113
|
-
|
|
116
|
+
# .diffctx/ignore patterns are relative to the directory that
|
|
117
|
+
# *contains* .diffctx/, not to .diffctx/ itself.
|
|
118
|
+
if ignore_path.parent.name == DIFFCTX_CONFIG_DIR and ignore_path.name == DIFFCTX_DIR_IGNORE:
|
|
119
|
+
ignore_dir = ignore_path.parent.parent
|
|
120
|
+
else:
|
|
121
|
+
ignore_dir = ignore_path.parent
|
|
114
122
|
rel = "" if ignore_dir == root else ignore_dir.relative_to(root).as_posix()
|
|
115
123
|
for line in read_ignore_file(ignore_path):
|
|
116
124
|
out.append(_process_ignore_line(line, rel))
|
|
@@ -146,9 +154,14 @@ def _aggregate_all_ignore_patterns(root: Path, ignore_filenames: list[str]) -> l
|
|
|
146
154
|
def _find_ignore_files_via_git(root: Path, ignore_filenames: list[str]) -> list[Path] | None:
|
|
147
155
|
import subprocess
|
|
148
156
|
|
|
157
|
+
# A bare filename pathspec (e.g. ".gitignore") only matches that exact
|
|
158
|
+
# path relative to `root` — it does NOT find nested occurrences. Use
|
|
159
|
+
# glob-magic pathspecs so nested .gitignore / .diffctx/ignore files are
|
|
160
|
+
# discovered the same way _collect_from_walk's os.walk fallback finds them.
|
|
161
|
+
pathspecs = [f":(glob)**/{name}" for name in ignore_filenames]
|
|
149
162
|
try:
|
|
150
163
|
result = subprocess.run(
|
|
151
|
-
["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", *
|
|
164
|
+
["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", *pathspecs],
|
|
152
165
|
cwd=root,
|
|
153
166
|
capture_output=True,
|
|
154
167
|
text=False,
|
|
@@ -315,7 +328,7 @@ def get_ignore_specs(
|
|
|
315
328
|
patterns.extend(DEFAULT_IGNORE_PATTERNS)
|
|
316
329
|
|
|
317
330
|
patterns.extend(_collect_parent_ignore_patterns(root_dir, [".gitignore"]))
|
|
318
|
-
patterns.extend(_aggregate_all_ignore_patterns(root_dir, [".gitignore"]))
|
|
331
|
+
patterns.extend(_aggregate_all_ignore_patterns(root_dir, [".gitignore", _DIFFCTX_IGNORE_RELPATH]))
|
|
319
332
|
|
|
320
333
|
if custom_ignore_file:
|
|
321
334
|
patterns.extend(read_ignore_file(custom_ignore_file))
|
|
@@ -31,7 +31,7 @@ def _configure_windows_utf8() -> None:
|
|
|
31
31
|
pass
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def _ensure_git_repo(root_dir: Path) -> None:
|
|
34
|
+
def _ensure_git_repo(root_dir: Path, prog: str) -> None:
|
|
35
35
|
try:
|
|
36
36
|
result = subprocess.run(
|
|
37
37
|
["git", "rev-parse", "--git-dir"],
|
|
@@ -42,18 +42,33 @@ def _ensure_git_repo(root_dir: Path) -> None:
|
|
|
42
42
|
)
|
|
43
43
|
except OSError as exc:
|
|
44
44
|
print(
|
|
45
|
-
f"
|
|
45
|
+
f"{prog}: --diff requires git to be installed and on PATH ({exc}); install git or run without --diff.",
|
|
46
46
|
file=sys.stderr,
|
|
47
47
|
)
|
|
48
48
|
sys.exit(_EXIT_ENVIRONMENT)
|
|
49
49
|
if result.returncode != 0:
|
|
50
50
|
print(
|
|
51
|
-
f"
|
|
51
|
+
f"{prog}: --diff requires a git repository (cwd: {root_dir}); "
|
|
52
52
|
"run inside a working tree or pass --diff <range> with a valid git context.",
|
|
53
53
|
file=sys.stderr,
|
|
54
54
|
)
|
|
55
55
|
sys.exit(_EXIT_ENVIRONMENT)
|
|
56
56
|
|
|
57
|
+
head_result = subprocess.run(
|
|
58
|
+
["git", "rev-parse", "--verify", "-q", "HEAD"],
|
|
59
|
+
cwd=str(root_dir),
|
|
60
|
+
stdout=subprocess.DEVNULL,
|
|
61
|
+
stderr=subprocess.DEVNULL,
|
|
62
|
+
check=False,
|
|
63
|
+
)
|
|
64
|
+
if head_result.returncode != 0:
|
|
65
|
+
print(
|
|
66
|
+
f"{prog}: --diff requires at least one commit (no HEAD in this repository yet); "
|
|
67
|
+
"commit first, or run without --diff to map the working tree.",
|
|
68
|
+
file=sys.stderr,
|
|
69
|
+
)
|
|
70
|
+
sys.exit(_EXIT_ENVIRONMENT)
|
|
71
|
+
|
|
57
72
|
|
|
58
73
|
def _diff_result_is_empty(result: dict[str, Any]) -> bool:
|
|
59
74
|
count = result.get("fragment_count")
|
|
@@ -65,21 +80,21 @@ def _diff_result_is_empty(result: dict[str, Any]) -> bool:
|
|
|
65
80
|
return False
|
|
66
81
|
|
|
67
82
|
|
|
68
|
-
def _warn_empty_diff_result(result: dict[str, Any]) -> None:
|
|
83
|
+
def _warn_empty_diff_result(result: dict[str, Any], prog: str) -> None:
|
|
69
84
|
if _diff_result_is_empty(result):
|
|
70
85
|
print(
|
|
71
|
-
"
|
|
86
|
+
f"{prog}: diff produced no semantic context "
|
|
72
87
|
"(pure deletion, binary-only, or all files exceeded size cap); output empty.",
|
|
73
88
|
file=sys.stderr,
|
|
74
89
|
)
|
|
75
90
|
|
|
76
91
|
|
|
77
|
-
def _build_diff_tree(args: ParsedArgs) -> dict[str, Any]:
|
|
92
|
+
def _build_diff_tree(args: ParsedArgs, prog: str) -> dict[str, Any]:
|
|
78
93
|
from .diffctx import build_diff_context
|
|
79
94
|
|
|
80
95
|
if not args.diff_range:
|
|
81
96
|
raise RuntimeError("diff_range is required in diff mode")
|
|
82
|
-
_ensure_git_repo(args.root_dir)
|
|
97
|
+
_ensure_git_repo(args.root_dir, prog)
|
|
83
98
|
result = build_diff_context(
|
|
84
99
|
root_dir=args.root_dir,
|
|
85
100
|
diff_range=args.diff_range,
|
|
@@ -93,7 +108,7 @@ def _build_diff_tree(args: ParsedArgs) -> dict[str, Any]:
|
|
|
93
108
|
whitelist_file=args.whitelist_file,
|
|
94
109
|
scoring_mode=getattr(args, "scoring", "ego"),
|
|
95
110
|
)
|
|
96
|
-
_warn_empty_diff_result(result)
|
|
111
|
+
_warn_empty_diff_result(result, prog)
|
|
97
112
|
return result
|
|
98
113
|
|
|
99
114
|
|
|
@@ -103,13 +118,19 @@ def _root_display_name(root_dir: Any) -> str:
|
|
|
103
118
|
|
|
104
119
|
|
|
105
120
|
_LARGE_OUTPUT_WARN_BYTES = 10 * 1024 * 1024
|
|
121
|
+
# A bare `diffctx` with no path argument at all is the most common "just try
|
|
122
|
+
# it" first invocation — and the easiest to run somewhere unintended (e.g.
|
|
123
|
+
# /tmp, $HOME). Use a much lower bar for that specific case; an explicit path
|
|
124
|
+
# argument is a stronger signal the user knows what they're pointing at (#87).
|
|
125
|
+
_LARGE_OUTPUT_WARN_BYTES_NO_EXPLICIT_PATH = 1 * 1024 * 1024
|
|
106
126
|
|
|
107
127
|
|
|
108
128
|
def _warn_if_output_oversized(output_content: str, args: ParsedArgs) -> None:
|
|
109
129
|
if args.no_content or args.diff_range:
|
|
110
130
|
return
|
|
131
|
+
threshold = _LARGE_OUTPUT_WARN_BYTES_NO_EXPLICIT_PATH if args.no_explicit_paths else _LARGE_OUTPUT_WARN_BYTES
|
|
111
132
|
size_bytes = len(output_content.encode("utf-8"))
|
|
112
|
-
if size_bytes <
|
|
133
|
+
if size_bytes < threshold:
|
|
113
134
|
return
|
|
114
135
|
mb = size_bytes / (1024 * 1024)
|
|
115
136
|
print(
|
|
@@ -312,7 +333,7 @@ def _run(argv: list[str] | None = None, *, prog: str = "diffctx", version: str =
|
|
|
312
333
|
sys.stdout.write(output_content)
|
|
313
334
|
return
|
|
314
335
|
|
|
315
|
-
directory_tree = _build_diff_tree(args) if args.diff_range else _build_standard_tree(args)
|
|
336
|
+
directory_tree = _build_diff_tree(args, prog) if args.diff_range else _build_standard_tree(args)
|
|
316
337
|
|
|
317
338
|
output_content = tree_to_string(directory_tree, args.output_format)
|
|
318
339
|
if not args.quiet:
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import argparse
|
|
3
4
|
import sys
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def main(prog: str = "diffctx-mcp", extra: str = "diffctx[mcp]") -> None:
|
|
8
|
+
parser = argparse.ArgumentParser(
|
|
9
|
+
prog=prog,
|
|
10
|
+
description="Run the diffctx MCP server (stdio transport) for editor/agent integration.",
|
|
11
|
+
)
|
|
12
|
+
parser.parse_args()
|
|
13
|
+
|
|
7
14
|
try:
|
|
8
15
|
from diffctx.mcp.server import run_server
|
|
9
16
|
except ImportError:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.10.2"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.10.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|