contextzip 0.4.4__tar.gz → 0.4.5__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.
- {contextzip-0.4.4 → contextzip-0.4.5}/PKG-INFO +6 -2
- {contextzip-0.4.4 → contextzip-0.4.5}/README.md +5 -1
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/__init__.py +1 -1
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/api.py +52 -2
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/cli.py +17 -11
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/cli_ai.py +1 -28
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/cli_display.py +4 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/error_parser.py +12 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/filters.py +116 -8
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/watcher.py +8 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip.egg-info/PKG-INFO +6 -2
- {contextzip-0.4.4 → contextzip-0.4.5}/pyproject.toml +1 -1
- {contextzip-0.4.4 → contextzip-0.4.5}/LICENSE +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/ai/__init__.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/ai/gemini.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/ai/heuristic.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/ai/selector.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/applier.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/cleanup.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/cli_onboard.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/clipboard.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/config.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/detector.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/git.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/packager.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/project_config.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/redact.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/report.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/rules/__init__.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/rules/base.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/rules/errors/__init__.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/rules/errors/node.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/rules/errors/python.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/rules/go.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/rules/node.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/rules/python.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/rules/ruby.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/rules/rust.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/updater.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/webui/__init__.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/webui/assets.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/webui/persist.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/webui/server.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip/webui/suggestions.py +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip.egg-info/SOURCES.txt +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip.egg-info/dependency_links.txt +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip.egg-info/entry_points.txt +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip.egg-info/requires.txt +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/contextzip.egg-info/top_level.txt +0 -0
- {contextzip-0.4.4 → contextzip-0.4.5}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: contextzip
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
4
4
|
Summary: Intelligently package your codebase for AI tools
|
|
5
5
|
Author-email: Deepesh <akadeepesh@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -27,7 +27,11 @@ Requires-Dist: pathspec>=0.12
|
|
|
27
27
|
Requires-Dist: httpx>=0.27
|
|
28
28
|
Dynamic: license-file
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
https://github.com/user-attachments/assets/91913ad6-6161-430d-9075-b0fa76498021
|
|
33
|
+
|
|
34
|
+
<!-- <img width="1536" height="1024" alt="cz" src="https://github.com/user-attachments/assets/a930b49c-34a0-447b-8f3a-1909eae977c8" /> -->
|
|
31
35
|
|
|
32
36
|
# contextzip
|
|
33
37
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
https://github.com/user-attachments/assets/91913ad6-6161-430d-9075-b0fa76498021
|
|
4
|
+
|
|
5
|
+
<!-- <img width="1536" height="1024" alt="cz" src="https://github.com/user-attachments/assets/a930b49c-34a0-447b-8f3a-1909eae977c8" /> -->
|
|
2
6
|
|
|
3
7
|
# contextzip
|
|
4
8
|
|
|
@@ -43,12 +43,15 @@ from contextzip.applier import ApplyResult
|
|
|
43
43
|
from contextzip.detector import DetectionResult, detect
|
|
44
44
|
from contextzip.filters import (
|
|
45
45
|
ResolveResult,
|
|
46
|
+
build_force_include_spec,
|
|
46
47
|
build_spec,
|
|
48
|
+
normalize_pattern,
|
|
47
49
|
resolve_files,
|
|
48
50
|
resolve_files_from_git,
|
|
49
51
|
)
|
|
50
52
|
from contextzip.git import GitError, GitErrorKind, get_changed_files
|
|
51
53
|
from contextzip.packager import PackageResult, create_zip_silent
|
|
54
|
+
from contextzip.project_config import load_project_config
|
|
52
55
|
|
|
53
56
|
|
|
54
57
|
# ---------------------------------------------------------------------------
|
|
@@ -139,19 +142,27 @@ class ZipNotFoundError(ContextzipError):
|
|
|
139
142
|
|
|
140
143
|
def get_git_changes(
|
|
141
144
|
path: str | Path | None = None,
|
|
145
|
+
*,
|
|
146
|
+
use_project_config: bool = True,
|
|
142
147
|
) -> FileCollection:
|
|
143
148
|
"""
|
|
144
149
|
Return the files that git reports as modified, added, or untracked.
|
|
145
150
|
|
|
146
151
|
The git root is found automatically by walking up from *path* (or the
|
|
147
152
|
current working directory). Files are filtered through contextzip's base
|
|
148
|
-
safety rules so secrets, binaries, and similar files are always excluded
|
|
153
|
+
safety rules so secrets, binaries, and similar files are always excluded,
|
|
154
|
+
and through the project's ``.contextzip/config.json``
|
|
155
|
+
(``always_include`` / ``always_exclude`` / ``limits``) — the same as the
|
|
156
|
+
CLI's ``--git-changes``.
|
|
149
157
|
|
|
150
158
|
Parameters
|
|
151
159
|
----------
|
|
152
160
|
path:
|
|
153
161
|
Directory to start from. Defaults to ``Path.cwd()``. The actual
|
|
154
162
|
git root may be a parent of this directory.
|
|
163
|
+
use_project_config:
|
|
164
|
+
Apply the project's ``.contextzip/config.json``. Defaults to
|
|
165
|
+
``True``; pass ``False`` to use only the built-in rules.
|
|
155
166
|
|
|
156
167
|
Returns
|
|
157
168
|
-------
|
|
@@ -192,9 +203,12 @@ def get_git_changes(
|
|
|
192
203
|
if git_result.is_empty:
|
|
193
204
|
return FileCollection(project_dir=project_dir)
|
|
194
205
|
|
|
206
|
+
cfg = load_project_config(project_dir) if use_project_config else None
|
|
207
|
+
|
|
195
208
|
resolved = resolve_files_from_git(
|
|
196
209
|
git_files=git_result.files,
|
|
197
210
|
project_dir=project_dir,
|
|
211
|
+
**_config_kwargs(cfg),
|
|
198
212
|
)
|
|
199
213
|
|
|
200
214
|
detection = detect(project_dir)
|
|
@@ -212,6 +226,7 @@ def get_files(
|
|
|
212
226
|
include: list[str] | None = None,
|
|
213
227
|
exclude: list[str] | None = None,
|
|
214
228
|
use_gitignore: bool = True,
|
|
229
|
+
use_project_config: bool = True,
|
|
215
230
|
) -> FileCollection:
|
|
216
231
|
"""
|
|
217
232
|
Return all project files after applying contextzip's standard exclusion rules.
|
|
@@ -229,6 +244,11 @@ def get_files(
|
|
|
229
244
|
use_gitignore:
|
|
230
245
|
Whether to apply the project's ``.gitignore`` file.
|
|
231
246
|
Defaults to ``True``.
|
|
247
|
+
use_project_config:
|
|
248
|
+
Apply the project's ``.contextzip/config.json`` (``always_include``,
|
|
249
|
+
``always_exclude``, ``limits``), the same as the CLI. Defaults to
|
|
250
|
+
``True``; pass ``False`` to use only the built-in rules and the
|
|
251
|
+
arguments above.
|
|
232
252
|
|
|
233
253
|
Returns
|
|
234
254
|
-------
|
|
@@ -256,9 +276,12 @@ def get_files(
|
|
|
256
276
|
|
|
257
277
|
gitignore_path = (project_dir / ".gitignore") if use_gitignore else None
|
|
258
278
|
|
|
279
|
+
cfg = load_project_config(project_dir) if use_project_config else None
|
|
280
|
+
cfg_kwargs = _config_kwargs(cfg, extra_exclude=exclude)
|
|
281
|
+
|
|
259
282
|
spec = build_spec(
|
|
260
283
|
rule_modules=detection.rule_modules,
|
|
261
|
-
extra_exclude=
|
|
284
|
+
extra_exclude=cfg_kwargs["extra_exclude"],
|
|
262
285
|
gitignore_path=gitignore_path,
|
|
263
286
|
)
|
|
264
287
|
|
|
@@ -266,6 +289,8 @@ def get_files(
|
|
|
266
289
|
project_dir=project_dir,
|
|
267
290
|
spec=spec,
|
|
268
291
|
include_only=include or None,
|
|
292
|
+
force_include=cfg_kwargs["force_include"],
|
|
293
|
+
large_file_warn_bytes=cfg_kwargs["large_file_warn_bytes"],
|
|
269
294
|
)
|
|
270
295
|
|
|
271
296
|
return _resolve_result_to_collection(resolved, project_dir, detection)
|
|
@@ -447,6 +472,31 @@ def detect_ecosystem(
|
|
|
447
472
|
# ---------------------------------------------------------------------------
|
|
448
473
|
|
|
449
474
|
|
|
475
|
+
def _config_kwargs(cfg, extra_exclude: list[str] | None = None) -> dict:
|
|
476
|
+
"""
|
|
477
|
+
Translate a ProjectConfig (or None) into the shared filter inputs:
|
|
478
|
+
extra_exclude (caller patterns + always_exclude), force_include
|
|
479
|
+
(always_include) and large_file_warn_bytes (limits.max_file_size_mb).
|
|
480
|
+
"""
|
|
481
|
+
from contextzip.filters import LARGE_FILE_WARN_BYTES
|
|
482
|
+
|
|
483
|
+
patterns = [normalize_pattern(p) for p in extra_exclude or []]
|
|
484
|
+
if cfg is None:
|
|
485
|
+
return {
|
|
486
|
+
"extra_exclude": patterns or None,
|
|
487
|
+
"force_include": None,
|
|
488
|
+
"large_file_warn_bytes": LARGE_FILE_WARN_BYTES,
|
|
489
|
+
}
|
|
490
|
+
patterns += [normalize_pattern(p) for p in cfg.always_exclude]
|
|
491
|
+
return {
|
|
492
|
+
"extra_exclude": patterns or None,
|
|
493
|
+
"force_include": build_force_include_spec(
|
|
494
|
+
[normalize_pattern(p) for p in cfg.always_include]
|
|
495
|
+
),
|
|
496
|
+
"large_file_warn_bytes": int(cfg.limits.max_file_size_mb * 1024 * 1024),
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
|
|
450
500
|
def _resolve_dir(path: str | Path | None) -> Path:
|
|
451
501
|
if path is None:
|
|
452
502
|
return Path(os.getcwd()).resolve()
|
|
@@ -927,6 +927,18 @@ def _run(
|
|
|
927
927
|
if saved:
|
|
928
928
|
project_cfg = load_project_config(project_dir)
|
|
929
929
|
|
|
930
|
+
# ── Include/exclude inputs shared by every scan mode ─────────────────────
|
|
931
|
+
# CLI --exclude/-e patterns plus any standing always_exclude patterns from
|
|
932
|
+
# project config — both are additive, persistent behavior comes from
|
|
933
|
+
# config.json so it doesn't need to be re-typed every run. always_include
|
|
934
|
+
# is a standing negation. Built once here so standard and --git-changes
|
|
935
|
+
# runs honour project config identically.
|
|
936
|
+
normalized_exclude = [normalize_pattern(p) for p in extra_exclude or []]
|
|
937
|
+
normalized_exclude += [normalize_pattern(p) for p in project_cfg.always_exclude]
|
|
938
|
+
force_include = build_force_include_spec(
|
|
939
|
+
[normalize_pattern(p) for p in project_cfg.always_include]
|
|
940
|
+
)
|
|
941
|
+
|
|
930
942
|
# ── Git-changes mode ─────────────────────────────────────────────────────
|
|
931
943
|
if git_changes:
|
|
932
944
|
with console.status("[cyan]Querying git for changed files…[/]", spinner="dots"):
|
|
@@ -939,7 +951,7 @@ def _run(
|
|
|
939
951
|
print_git_deleted_and_submodules(git_result)
|
|
940
952
|
|
|
941
953
|
if git_result.is_empty:
|
|
942
|
-
info("Nothing to
|
|
954
|
+
info("Nothing to compress in this one — working tree is clean.")
|
|
943
955
|
return
|
|
944
956
|
|
|
945
957
|
scan_label, scan_detail = git_scan_label_and_detail(git_result)
|
|
@@ -953,27 +965,21 @@ def _run(
|
|
|
953
965
|
large_file_warn_bytes=int(
|
|
954
966
|
project_cfg.limits.max_file_size_mb * 1024 * 1024
|
|
955
967
|
),
|
|
968
|
+
extra_exclude=normalized_exclude if normalized_exclude else None,
|
|
969
|
+
include_only=include_only if include_only else None,
|
|
970
|
+
force_include=force_include,
|
|
956
971
|
)
|
|
957
972
|
|
|
958
973
|
else:
|
|
959
974
|
# ── Build exclusion spec ─────────────────────────────────────────────
|
|
960
975
|
gitignore_path = None if no_gitignore else (project_dir / ".gitignore")
|
|
961
976
|
|
|
962
|
-
# CLI --exclude/-e patterns plus any standing always_exclude patterns
|
|
963
|
-
# from project config — both are additive, persistent behavior comes
|
|
964
|
-
# from config.json so it doesn't need to be re-typed every run.
|
|
965
|
-
normalized_exclude = [normalize_pattern(p) for p in extra_exclude or []]
|
|
966
|
-
normalized_exclude += [normalize_pattern(p) for p in project_cfg.always_exclude]
|
|
967
|
-
|
|
968
977
|
with console.status("[cyan]Building exclusion rules…[/]", spinner="dots"):
|
|
969
978
|
spec = build_spec(
|
|
970
979
|
rule_modules=detection.rule_modules,
|
|
971
980
|
extra_exclude=normalized_exclude if normalized_exclude else None,
|
|
972
981
|
gitignore_path=gitignore_path,
|
|
973
982
|
)
|
|
974
|
-
force_include = build_force_include_spec(
|
|
975
|
-
[normalize_pattern(p) for p in project_cfg.always_include]
|
|
976
|
-
)
|
|
977
983
|
|
|
978
984
|
# ── Resolve files ─────────────────────────────────────────────────────
|
|
979
985
|
with console.status("[cyan]Scanning project files…[/]", spinner="dots"):
|
|
@@ -1027,7 +1033,7 @@ def _run(
|
|
|
1027
1033
|
return
|
|
1028
1034
|
|
|
1029
1035
|
if not resolved.included:
|
|
1030
|
-
err("Nothing to
|
|
1036
|
+
err("Nothing to compress in this one — all files were excluded.")
|
|
1031
1037
|
info("Try contextzip include PATH or -i PATH to override.")
|
|
1032
1038
|
return
|
|
1033
1039
|
|
|
@@ -15,38 +15,11 @@ from pathlib import Path
|
|
|
15
15
|
from rich.console import Console
|
|
16
16
|
|
|
17
17
|
from contextzip.cli_display import print_ai_selection, err, warn, info
|
|
18
|
+
from contextzip.filters import normalize_pattern # noqa: F401 (re-exported for cli.py)
|
|
18
19
|
|
|
19
20
|
console = Console()
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
# ---------------------------------------------------------------------------
|
|
23
|
-
# Pattern normalisation
|
|
24
|
-
# ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def normalize_pattern(p: str) -> str:
|
|
28
|
-
"""
|
|
29
|
-
Canonicalise a user-supplied exclusion pattern.
|
|
30
|
-
|
|
31
|
-
Rules applied in order:
|
|
32
|
-
1. Strip a leading ``./`` or ``.\\`` so that ``./CHANGELOG.md``
|
|
33
|
-
and ``CHANGELOG.md`` are treated identically.
|
|
34
|
-
2. Replace every backslash with a forward slash for cross-platform
|
|
35
|
-
consistency (Windows paths entered on the CLI).
|
|
36
|
-
3. Collapse ``folder/*`` → ``folder/`` so that gitignore-style
|
|
37
|
-
directory globs work as expected.
|
|
38
|
-
"""
|
|
39
|
-
# 1. Strip leading ./ or .\\
|
|
40
|
-
if p.startswith("./") or p.startswith(".\\"):
|
|
41
|
-
p = p[2:]
|
|
42
|
-
# 2. Normalise path separators
|
|
43
|
-
p = p.replace("\\", "/")
|
|
44
|
-
# 3. folder/* → folder/
|
|
45
|
-
if p.endswith("/*"):
|
|
46
|
-
p = p[:-1]
|
|
47
|
-
return p
|
|
48
|
-
|
|
49
|
-
|
|
50
23
|
# ---------------------------------------------------------------------------
|
|
51
24
|
# AI selection (full run — produces ZIP)
|
|
52
25
|
# ---------------------------------------------------------------------------
|
|
@@ -64,6 +64,10 @@ def print_detection(detection, *, con: Console = console) -> None:
|
|
|
64
64
|
"""One line: what was detected and how confident contextzip is."""
|
|
65
65
|
if detection.is_unknown:
|
|
66
66
|
ok("Detected", "unknown ecosystem, base rules only", con=con)
|
|
67
|
+
info(
|
|
68
|
+
"Don't see your language? Add it here: https://example.com",
|
|
69
|
+
con=con,
|
|
70
|
+
)
|
|
67
71
|
return
|
|
68
72
|
|
|
69
73
|
parts = []
|
|
@@ -22,6 +22,8 @@ from __future__ import annotations
|
|
|
22
22
|
import re
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
|
|
25
|
+
from contextzip.filters import filter_paths_by_config
|
|
26
|
+
|
|
25
27
|
# ---------------------------------------------------------------------------
|
|
26
28
|
# ANSI escape code stripping
|
|
27
29
|
# ---------------------------------------------------------------------------
|
|
@@ -453,6 +455,8 @@ def process_buffer(
|
|
|
453
455
|
project_dir: Path,
|
|
454
456
|
ecosystems: list[str],
|
|
455
457
|
ecosystem_display: str,
|
|
458
|
+
always_exclude: list[str] | None = None,
|
|
459
|
+
always_include: list[str] | None = None,
|
|
456
460
|
) -> tuple[str, str, list[Path]] | None:
|
|
457
461
|
"""
|
|
458
462
|
Run the full pipeline on *raw_buffer*.
|
|
@@ -472,6 +476,11 @@ def process_buffer(
|
|
|
472
476
|
List of detected ecosystem names from DetectionResult.ecosystems.
|
|
473
477
|
ecosystem_display:
|
|
474
478
|
Human-readable display string, e.g. "Next.js + Node.js".
|
|
479
|
+
always_exclude / always_include:
|
|
480
|
+
A project config's standing patterns. Files named in the stack trace
|
|
481
|
+
that match always_exclude are left out of the referenced-file list
|
|
482
|
+
(and therefore out of source-files.zip and prompt.txt), unless they
|
|
483
|
+
also match always_include.
|
|
475
484
|
"""
|
|
476
485
|
# 1. Strip ANSI
|
|
477
486
|
clean_text = strip_ansi(raw_buffer)
|
|
@@ -493,6 +502,9 @@ def process_buffer(
|
|
|
493
502
|
|
|
494
503
|
# 4. Extract file paths
|
|
495
504
|
referenced_paths = extract_paths(error_block_raw, project_dir, ecosystems)
|
|
505
|
+
referenced_paths = filter_paths_by_config(
|
|
506
|
+
referenced_paths, project_dir, always_exclude, always_include
|
|
507
|
+
)
|
|
496
508
|
|
|
497
509
|
# 5. Build prompt.txt
|
|
498
510
|
prompt_txt = build_prompt_txt(
|
|
@@ -12,14 +12,18 @@ Phase 5 hardening:
|
|
|
12
12
|
Git mode:
|
|
13
13
|
- resolve_files_from_git() accepts a GitChanges result and builds a
|
|
14
14
|
ResolveResult from only the modified/added/untracked files reported
|
|
15
|
-
by git, while still running all size and binary checks.
|
|
15
|
+
by git, while still running all size and binary checks. It honours the
|
|
16
|
+
same extra-exclude / include-only / force-include inputs as
|
|
17
|
+
resolve_files(), so project config (always_include / always_exclude)
|
|
18
|
+
and CLI --include/--exclude behave identically with and without
|
|
19
|
+
--git-changes.
|
|
16
20
|
|
|
17
21
|
Force-include (Phase 7):
|
|
18
22
|
- build_force_include_spec() turns a project config's "always_include"
|
|
19
|
-
patterns into a PathSpec. resolve_files()
|
|
20
|
-
and, when a file (or one of its parent
|
|
21
|
-
the file as not excluded — a standing
|
|
22
|
-
|
|
23
|
+
patterns into a PathSpec. resolve_files() and resolve_files_from_git()
|
|
24
|
+
accept it as force_include and, when a file (or one of its parent
|
|
25
|
+
directories) matches, treat the file as not excluded — a standing
|
|
26
|
+
negation on top of auto-rules, .gitignore and --exclude. It does not
|
|
23
27
|
override an explicit --include/-i for the current run.
|
|
24
28
|
"""
|
|
25
29
|
|
|
@@ -106,6 +110,27 @@ class ResolveResult:
|
|
|
106
110
|
# ---------------------------------------------------------------------------
|
|
107
111
|
|
|
108
112
|
|
|
113
|
+
def normalize_pattern(p: str) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Canonicalise a user-supplied exclusion/inclusion pattern (CLI flag or
|
|
116
|
+
project config entry).
|
|
117
|
+
|
|
118
|
+
Rules applied in order:
|
|
119
|
+
1. Strip a leading ``./`` or ``.\\`` so that ``./CHANGELOG.md``
|
|
120
|
+
and ``CHANGELOG.md`` are treated identically.
|
|
121
|
+
2. Replace every backslash with a forward slash for cross-platform
|
|
122
|
+
consistency (Windows paths entered on the CLI).
|
|
123
|
+
3. Collapse ``folder/*`` → ``folder/`` so that gitignore-style
|
|
124
|
+
directory globs work as expected.
|
|
125
|
+
"""
|
|
126
|
+
if p.startswith("./") or p.startswith(".\\"):
|
|
127
|
+
p = p[2:]
|
|
128
|
+
p = p.replace("\\", "/")
|
|
129
|
+
if p.endswith("/*"):
|
|
130
|
+
p = p[:-1]
|
|
131
|
+
return p
|
|
132
|
+
|
|
133
|
+
|
|
109
134
|
def build_spec(
|
|
110
135
|
rule_modules: list[str],
|
|
111
136
|
extra_exclude: list[str] | None = None,
|
|
@@ -368,6 +393,9 @@ def resolve_files_from_git(
|
|
|
368
393
|
git_files: list[Path],
|
|
369
394
|
project_dir: Path,
|
|
370
395
|
large_file_warn_bytes: int = LARGE_FILE_WARN_BYTES,
|
|
396
|
+
extra_exclude: list[str] | None = None,
|
|
397
|
+
include_only: list[str] | None = None,
|
|
398
|
+
force_include: pathspec.PathSpec | None = None,
|
|
371
399
|
) -> ResolveResult:
|
|
372
400
|
"""
|
|
373
401
|
Build a :class:`ResolveResult` from a pre-selected list of files reported
|
|
@@ -378,6 +406,16 @@ def resolve_files_from_git(
|
|
|
378
406
|
Size and binary checks are also performed so that the usual warnings appear
|
|
379
407
|
in the CLI output.
|
|
380
408
|
|
|
409
|
+
Precedence mirrors :func:`resolve_files` exactly, so a project's config
|
|
410
|
+
behaves the same with and without ``--git-changes``:
|
|
411
|
+
|
|
412
|
+
1. *force_include* (config ``always_include``) — a match short-circuits
|
|
413
|
+
every exclusion check below, including the base safety floor.
|
|
414
|
+
2. Base safety floor + *extra_exclude* (CLI ``--exclude`` plus config
|
|
415
|
+
``always_exclude``) — a match excludes the file.
|
|
416
|
+
3. *include_only* (CLI ``--include``) — the file must live under one of
|
|
417
|
+
these paths, even if it was force-included.
|
|
418
|
+
|
|
381
419
|
Parameters
|
|
382
420
|
----------
|
|
383
421
|
git_files:
|
|
@@ -388,10 +426,18 @@ def resolve_files_from_git(
|
|
|
388
426
|
large_file_warn_bytes:
|
|
389
427
|
Overrides the module default (1 MB) — typically a project's
|
|
390
428
|
`limits.max_file_size_mb` preference (.contextzip/config.json).
|
|
429
|
+
extra_exclude:
|
|
430
|
+
Additional gitignore-style exclusion patterns, layered on top of the
|
|
431
|
+
base safety floor.
|
|
432
|
+
include_only:
|
|
433
|
+
If given, only files under these path prefixes are kept.
|
|
434
|
+
force_include:
|
|
435
|
+
Spec from :func:`build_force_include_spec`; matching files skip all
|
|
436
|
+
exclusion checks.
|
|
391
437
|
"""
|
|
392
438
|
# Build the base spec once — this is our safety floor regardless of what
|
|
393
439
|
# git reports as changed. It blocks .env, secrets, binaries, etc.
|
|
394
|
-
base_spec = build_spec(rule_modules=["base"])
|
|
440
|
+
base_spec = build_spec(rule_modules=["base"], extra_exclude=extra_exclude or None)
|
|
395
441
|
|
|
396
442
|
result = ResolveResult()
|
|
397
443
|
|
|
@@ -417,9 +463,23 @@ def resolve_files_from_git(
|
|
|
417
463
|
result.skipped.append((abs_path, "outside project tree"))
|
|
418
464
|
continue
|
|
419
465
|
|
|
420
|
-
# ── Base safety floor — block secrets / binaries even if git-tracked ─
|
|
421
466
|
rel_str = rel.as_posix()
|
|
422
|
-
|
|
467
|
+
|
|
468
|
+
# ── force_include (config always_include) short-circuits exclusion ───
|
|
469
|
+
forced = force_include is not None and (
|
|
470
|
+
force_include.match_file(rel_str)
|
|
471
|
+
or _any_parent_excluded(rel, force_include)
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# ── Base safety floor + extra excludes (CLI --exclude / always_exclude)
|
|
475
|
+
if not forced and (
|
|
476
|
+
_any_parent_excluded(rel, base_spec) or base_spec.match_file(rel_str)
|
|
477
|
+
):
|
|
478
|
+
result.excluded.append(abs_path)
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
# ── Apply --include filter (exact prefix, not substring) ─────────────
|
|
482
|
+
if include_only and not _matches_any_prefix(rel_str, include_only):
|
|
423
483
|
result.excluded.append(abs_path)
|
|
424
484
|
continue
|
|
425
485
|
|
|
@@ -443,6 +503,54 @@ def resolve_files_from_git(
|
|
|
443
503
|
return result
|
|
444
504
|
|
|
445
505
|
|
|
506
|
+
def filter_paths_by_config(
|
|
507
|
+
paths: list[Path],
|
|
508
|
+
project_dir: Path,
|
|
509
|
+
always_exclude: list[str] | None = None,
|
|
510
|
+
always_include: list[str] | None = None,
|
|
511
|
+
) -> list[Path]:
|
|
512
|
+
"""
|
|
513
|
+
Drop any of *paths* matched by a project config's ``always_exclude``,
|
|
514
|
+
unless it's also matched by ``always_include`` (same precedence as
|
|
515
|
+
:func:`resolve_files`: force-include wins).
|
|
516
|
+
|
|
517
|
+
For callers that pick files by some other means than a directory scan
|
|
518
|
+
(e.g. ``contextzip watch`` collects files named in a stack trace) but
|
|
519
|
+
must still respect the user's manual include/exclude choices. Paths
|
|
520
|
+
outside *project_dir* are left untouched.
|
|
521
|
+
"""
|
|
522
|
+
if not always_exclude:
|
|
523
|
+
return list(paths)
|
|
524
|
+
|
|
525
|
+
exclude_spec = build_spec(
|
|
526
|
+
rule_modules=[],
|
|
527
|
+
extra_exclude=[normalize_pattern(p) for p in always_exclude],
|
|
528
|
+
)
|
|
529
|
+
force_spec = build_force_include_spec(
|
|
530
|
+
[normalize_pattern(p) for p in always_include or []]
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
kept: list[Path] = []
|
|
534
|
+
for path in paths:
|
|
535
|
+
try:
|
|
536
|
+
rel = path.relative_to(project_dir)
|
|
537
|
+
except ValueError:
|
|
538
|
+
kept.append(path)
|
|
539
|
+
continue
|
|
540
|
+
rel_str = rel.as_posix()
|
|
541
|
+
|
|
542
|
+
forced = force_spec is not None and (
|
|
543
|
+
force_spec.match_file(rel_str) or _any_parent_excluded(rel, force_spec)
|
|
544
|
+
)
|
|
545
|
+
excluded = _any_parent_excluded(rel, exclude_spec) or exclude_spec.match_file(
|
|
546
|
+
rel_str
|
|
547
|
+
)
|
|
548
|
+
if excluded and not forced:
|
|
549
|
+
continue
|
|
550
|
+
kept.append(path)
|
|
551
|
+
return kept
|
|
552
|
+
|
|
553
|
+
|
|
446
554
|
def summarise_exclusions(excluded: list[Path], project_dir: Path) -> dict[str, int]:
|
|
447
555
|
"""Group excluded files by top-level directory / filename, sorted by count."""
|
|
448
556
|
buckets: dict[str, int] = {}
|
|
@@ -513,11 +513,19 @@ def _package_debug_context(
|
|
|
513
513
|
)
|
|
514
514
|
referenced_paths: list[Path] = []
|
|
515
515
|
else:
|
|
516
|
+
# Honour the project's manual include/exclude choices — a file
|
|
517
|
+
# the user put in always_exclude must not leak into the debug
|
|
518
|
+
# zip just because a stack trace happened to mention it.
|
|
519
|
+
from contextzip.project_config import load_project_config
|
|
520
|
+
|
|
521
|
+
project_cfg = load_project_config(project_dir)
|
|
516
522
|
result = process_buffer(
|
|
517
523
|
raw_buffer=raw_text,
|
|
518
524
|
project_dir=project_dir,
|
|
519
525
|
ecosystems=ecosystems,
|
|
520
526
|
ecosystem_display=ecosystem_display,
|
|
527
|
+
always_exclude=project_cfg.always_exclude,
|
|
528
|
+
always_include=project_cfg.always_include,
|
|
521
529
|
)
|
|
522
530
|
|
|
523
531
|
if result is None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: contextzip
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
4
4
|
Summary: Intelligently package your codebase for AI tools
|
|
5
5
|
Author-email: Deepesh <akadeepesh@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -27,7 +27,11 @@ Requires-Dist: pathspec>=0.12
|
|
|
27
27
|
Requires-Dist: httpx>=0.27
|
|
28
28
|
Dynamic: license-file
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
https://github.com/user-attachments/assets/91913ad6-6161-430d-9075-b0fa76498021
|
|
33
|
+
|
|
34
|
+
<!-- <img width="1536" height="1024" alt="cz" src="https://github.com/user-attachments/assets/a930b49c-34a0-447b-8f3a-1909eae977c8" /> -->
|
|
31
35
|
|
|
32
36
|
# contextzip
|
|
33
37
|
|
|
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
|