Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| 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: | |
| # gitignore_parser expects paths as strings; absolute paths are safest here. | |
| 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() | |
| # If out_dir is inside repo_root, compute its rel path so we can prune traversal into it. | |
| out_rel: Path | None = None | |
| try: | |
| out_rel = out_dir.relative_to(repo_root) | |
| except ValueError: | |
| out_rel = None | |
| # Map directory -> cumulative rules that apply inside that directory. | |
| 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 | |
| # Hard excludes | |
| 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 | |
| # Build cumulative rules for this directory: parent rules + local .gitignore rules | |
| 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 | |
| # Filter directories in-place to control traversal | |
| 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 | |
| # Ensure destination dir exists | |
| (out_dir / rel_dir).mkdir(parents=True, exist_ok=True) | |
| # Copy files | |
| 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() | |