From f213a3c28216a75218ef465eecc9a3ce16a3bcc8 Mon Sep 17 00:00:00 2001 From: akadeepesh Date: Wed, 7 Oct 2026 11:17:15 +0530 Subject: [PATCH] feat(api): enhance git change retrieval with project config support --- contextzip/api.py | 54 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/contextzip/api.py b/contextzip/api.py index 1de196b..1b918dc 100644 --- a/contextzip/api.py +++ b/contextzip/api.py @@ -43,12 +43,15 @@ from contextzip.applier import ApplyResult from contextzip.detector import DetectionResult, detect from contextzip.filters import ( ResolveResult, + build_force_include_spec, build_spec, + normalize_pattern, resolve_files, resolve_files_from_git, ) from contextzip.git import GitError, GitErrorKind, get_changed_files from contextzip.packager import PackageResult, create_zip_silent +from contextzip.project_config import load_project_config # --------------------------------------------------------------------------- @@ -139,19 +142,27 @@ class ZipNotFoundError(ContextzipError): def get_git_changes( path: str | Path | None = None, + *, + use_project_config: bool = True, ) -> FileCollection: """ Return the files that git reports as modified, added, or untracked. The git root is found automatically by walking up from *path* (or the current working directory). Files are filtered through contextzip's base - safety rules so secrets, binaries, and similar files are always excluded. + safety rules so secrets, binaries, and similar files are always excluded, + and through the project's ``.contextzip/config.json`` + (``always_include`` / ``always_exclude`` / ``limits``) — the same as the + CLI's ``--git-changes``. Parameters ---------- path: Directory to start from. Defaults to ``Path.cwd()``. The actual git root may be a parent of this directory. + use_project_config: + Apply the project's ``.contextzip/config.json``. Defaults to + ``True``; pass ``False`` to use only the built-in rules. Returns ------- @@ -192,9 +203,12 @@ def get_git_changes( if git_result.is_empty: return FileCollection(project_dir=project_dir) + cfg = load_project_config(project_dir) if use_project_config else None + resolved = resolve_files_from_git( git_files=git_result.files, project_dir=project_dir, + **_config_kwargs(cfg), ) detection = detect(project_dir) @@ -212,6 +226,7 @@ def get_files( include: list[str] | None = None, exclude: list[str] | None = None, use_gitignore: bool = True, + use_project_config: bool = True, ) -> FileCollection: """ Return all project files after applying contextzip's standard exclusion rules. @@ -229,6 +244,11 @@ def get_files( use_gitignore: Whether to apply the project's ``.gitignore`` file. Defaults to ``True``. + use_project_config: + Apply the project's ``.contextzip/config.json`` (``always_include``, + ``always_exclude``, ``limits``), the same as the CLI. Defaults to + ``True``; pass ``False`` to use only the built-in rules and the + arguments above. Returns ------- @@ -256,9 +276,12 @@ def get_files( gitignore_path = (project_dir / ".gitignore") if use_gitignore else None + cfg = load_project_config(project_dir) if use_project_config else None + cfg_kwargs = _config_kwargs(cfg, extra_exclude=exclude) + spec = build_spec( rule_modules=detection.rule_modules, - extra_exclude=exclude or None, + extra_exclude=cfg_kwargs["extra_exclude"], gitignore_path=gitignore_path, ) @@ -266,6 +289,8 @@ def get_files( project_dir=project_dir, spec=spec, include_only=include or None, + force_include=cfg_kwargs["force_include"], + large_file_warn_bytes=cfg_kwargs["large_file_warn_bytes"], ) return _resolve_result_to_collection(resolved, project_dir, detection) @@ -447,6 +472,31 @@ def detect_ecosystem( # --------------------------------------------------------------------------- +def _config_kwargs(cfg, extra_exclude: list[str] | None = None) -> dict: + """ + Translate a ProjectConfig (or None) into the shared filter inputs: + extra_exclude (caller patterns + always_exclude), force_include + (always_include) and large_file_warn_bytes (limits.max_file_size_mb). + """ + from contextzip.filters import LARGE_FILE_WARN_BYTES + + patterns = [normalize_pattern(p) for p in extra_exclude or []] + if cfg is None: + return { + "extra_exclude": patterns or None, + "force_include": None, + "large_file_warn_bytes": LARGE_FILE_WARN_BYTES, + } + patterns += [normalize_pattern(p) for p in cfg.always_exclude] + return { + "extra_exclude": patterns or None, + "force_include": build_force_include_spec( + [normalize_pattern(p) for p in cfg.always_include] + ), + "large_file_warn_bytes": int(cfg.limits.max_file_size_mb * 1024 * 1024), + } + + def _resolve_dir(path: str | Path | None) -> Path: if path is None: return Path(os.getcwd()).resolve()