Files
Nexus/scripts/generate_release_diff.py
T
2026-07-08 22:31:31 +08:00

111 lines
3.2 KiB
Python

#!/usr/bin/env python3
"""Generate a release diff manifest between two Nexus source snapshots."""
from __future__ import annotations
import argparse
import hashlib
from pathlib import Path
DEFAULT_EXCLUDE_NAMES = {
".git",
"node_modules",
".venv-py314",
".venv-py312-codex",
"venv",
"__pycache__",
".ruff_cache",
".playwright-mcp",
".cursor",
".pytest_cache",
"tmp",
}
DEFAULT_EXCLUDE_SUFFIXES = {".pyc", ".pyo"}
DEFAULT_EXCLUDE_EXACT = {"2025.2", "=2025.2"}
DEFAULT_EXCLUDE_PATTERNS = (".bak-",)
def sha256_file(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as fh:
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
def snapshot_files(base: Path) -> dict[str, str]:
result: dict[str, str] = {}
for path in base.rglob("*"):
if not path.is_file():
continue
rel = path.relative_to(base).as_posix()
parts = set(Path(rel).parts)
if parts & DEFAULT_EXCLUDE_NAMES:
continue
if path.suffix in DEFAULT_EXCLUDE_SUFFIXES:
continue
if rel in DEFAULT_EXCLUDE_EXACT:
continue
if any(rel.endswith(pattern) for pattern in DEFAULT_EXCLUDE_PATTERNS):
continue
result[rel] = sha256_file(path)
return result
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--old", required=True, type=Path)
parser.add_argument("--new", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
args = parser.parse_args()
old = args.old.resolve()
new = args.new.resolve()
old_files = snapshot_files(old)
new_files = snapshot_files(new)
added = sorted(set(new_files) - set(old_files))
deleted = sorted(set(old_files) - set(new_files))
modified = sorted(k for k in set(old_files) & set(new_files) if old_files[k] != new_files[k])
lines: list[str] = [
"# Nexus release diff manifest (2026-07-08)",
"",
"Baseline:",
"",
f"- Old snapshot: `{old}`",
f"- New snapshot: `{new}`",
"",
"Excluded names/patterns: `.git`, `node_modules`, `.venv-*`, `venv`, `__pycache__`, `.ruff_cache`, `.playwright-mcp`, `.cursor`, `.pytest_cache`, `tmp`, `*.pyc`, `*.pyo`, `*.bak-`, `2025.2`, `=2025.2`.",
"",
"## Summary",
"",
f"- Added files: {len(added)}",
f"- Modified files: {len(modified)}",
f"- Deleted files: {len(deleted)}",
"",
]
for title, items in (
("Added files", added),
("Modified files", modified),
("Deleted files", deleted),
):
lines.extend([f"## {title}", ""])
if items:
lines.extend(f"- `{item}`" for item in items)
else:
lines.append("- None")
lines.append("")
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text("\n".join(lines), encoding="utf-8")
print(args.output)
print(f"added={len(added)} modified={len(modified)} deleted={len(deleted)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())