Spaces:
Running
Running
File size: 4,484 Bytes
0f8b3a0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | #!/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()
|