| | |
| | from __future__ import annotations |
| |
|
| | import argparse |
| | import os |
| | import shutil |
| | from pathlib import Path |
| |
|
| | from gitignore_parser import handle_negation, rule_from_pattern |
| |
|
| |
|
| | def _load_gitignore_rules(gitignore_path: Path) -> list: |
| | """ |
| | Parse a single .gitignore into gitignore_parser IgnoreRule objects. |
| | |
| | We intentionally use gitignore_parser's internal rule representation so we can |
| | combine multiple .gitignore files (root + subfolders) while keeping correct |
| | precedence (later rules override earlier ones). |
| | """ |
| | base_dir = gitignore_path.parent |
| |
|
| | try: |
| | lines = gitignore_path.read_text(encoding="utf-8").splitlines() |
| | except OSError: |
| | return [] |
| |
|
| | rules = [] |
| | for raw in lines: |
| | line = raw.strip() |
| | if not line or line.startswith("#"): |
| | continue |
| | rules.append(rule_from_pattern(line, base_path=str(base_dir))) |
| | return rules |
| |
|
| |
|
| | def _is_ignored(abs_path: Path, rules: list) -> bool: |
| | |
| | return bool(handle_negation(str(abs_path), rules)) |
| |
|
| |
|
| | def _copy_repo_respecting_gitignores(repo_root: Path, out_dir: Path) -> None: |
| | repo_root = repo_root.resolve() |
| | out_dir = out_dir.resolve() |
| |
|
| | |
| | out_rel: Path | None = None |
| | try: |
| | out_rel = out_dir.relative_to(repo_root) |
| | except ValueError: |
| | out_rel = None |
| |
|
| | |
| | rules_for_dir: dict[Path, list] = {} |
| |
|
| | for current_dir, dirnames, filenames in os.walk(repo_root, topdown=True): |
| | current_path = Path(current_dir) |
| |
|
| | try: |
| | rel_dir = current_path.resolve().relative_to(repo_root) |
| | except ValueError: |
| | continue |
| |
|
| | |
| | if rel_dir == Path(".git") or Path(".git") in rel_dir.parents: |
| | dirnames[:] = [] |
| | continue |
| | if out_rel is not None and (rel_dir == out_rel or out_rel in rel_dir.parents): |
| | dirnames[:] = [] |
| | continue |
| |
|
| | |
| | parent_rel = rel_dir.parent if rel_dir != Path(".") else None |
| | parent_rules = rules_for_dir.get(parent_rel, []) if parent_rel is not None else [] |
| |
|
| | local_rules = [] |
| | local_gitignore = current_path / ".gitignore" |
| | if local_gitignore.is_file(): |
| | local_rules = _load_gitignore_rules(local_gitignore) |
| |
|
| | current_rules = [*parent_rules, *local_rules] |
| | rules_for_dir[rel_dir] = current_rules |
| |
|
| | |
| | kept_dirs: list[str] = [] |
| | for d in dirnames: |
| | rel = rel_dir / d |
| |
|
| | if rel == Path(".git") or Path(".git") in rel.parents: |
| | continue |
| | if out_rel is not None and (rel == out_rel or out_rel in rel.parents): |
| | continue |
| |
|
| | abs_path = (repo_root / rel).resolve() |
| | if _is_ignored(abs_path, current_rules): |
| | continue |
| |
|
| | kept_dirs.append(d) |
| | dirnames[:] = kept_dirs |
| |
|
| | |
| | (out_dir / rel_dir).mkdir(parents=True, exist_ok=True) |
| |
|
| | |
| | for f in filenames: |
| | rel_file = rel_dir / f |
| | abs_file = (repo_root / rel_file).resolve() |
| |
|
| | if _is_ignored(abs_file, current_rules): |
| | continue |
| |
|
| | src = repo_root / rel_file |
| | dst = out_dir / rel_file |
| | dst.parent.mkdir(parents=True, exist_ok=True) |
| | shutil.copy2(src, dst) |
| |
|
| |
|
| | def main() -> None: |
| | parser = argparse.ArgumentParser( |
| | description="Build a release directory by copying all files except those ignored by .gitignore files." |
| | ) |
| | parser.add_argument("--out-dir", default="out", help="Output directory.") |
| | parser.add_argument("--clean", action="store_true", help="Delete output directory before building.") |
| | args = parser.parse_args() |
| |
|
| | repo_root = Path.cwd() |
| | out_dir = (repo_root / args.out_dir).resolve() |
| |
|
| | if args.clean and out_dir.exists(): |
| | shutil.rmtree(out_dir) |
| | out_dir.mkdir(parents=True, exist_ok=True) |
| |
|
| | _copy_repo_respecting_gitignores(repo_root=repo_root, out_dir=out_dir) |
| | print(f"Release directory built at: {out_dir}") |
| |
|
| |
|
| | if __name__ == "__main__": |
| | main() |
| |
|