#!/usr/bin/env python3 """Diff original/working files in a 1C patch workspace.""" from __future__ import annotations import argparse import difflib import hashlib import json from pathlib import Path from typing import Any from check_1c_patch_workspace_integrity import check_workspace def load_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8-sig")) def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as stream: for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def read_text_lines(path: Path) -> list[str]: try: return path.read_text(encoding="utf-8-sig").splitlines(keepends=True) except UnicodeDecodeError: return path.read_text(encoding="cp1251", errors="replace").splitlines(keepends=True) def count_diff_lines(diff_lines: list[str]) -> dict[str, int]: added = 0 removed = 0 hunks = 0 for line in diff_lines: if line.startswith("@@"): hunks += 1 elif line.startswith("+") and not line.startswith("+++"): added += 1 elif line.startswith("-") and not line.startswith("---"): removed += 1 return {"added_lines": added, "removed_lines": removed, "hunks": hunks} def diff_file(record: dict[str, Any], *, workspace: Path, max_patch_chars: int) -> dict[str, Any]: rel = Path(str(record.get("relative_path") or "")) original = workspace / "original" / rel working = workspace / "working" / rel result = { "relative_path": str(rel).replace("\\", "/"), "kind": record.get("kind"), "name": record.get("name"), "origin": record.get("origin"), "original_path": str(original), "working_path": str(working), "exists": {"original": original.exists(), "working": working.exists()}, } if not original.exists() or not working.exists(): result["status"] = "missing_file" return result original_hash = sha256_file(original) working_hash = sha256_file(working) result["sha256"] = {"original": original_hash, "working": working_hash} if original_hash == working_hash: result["status"] = "unchanged" result["diff"] = {"added_lines": 0, "removed_lines": 0, "hunks": 0, "patch": ""} return result old_lines = read_text_lines(original) new_lines = read_text_lines(working) diff_lines = list( difflib.unified_diff( old_lines, new_lines, fromfile=f"original/{result['relative_path']}", tofile=f"working/{result['relative_path']}", lineterm="", ) ) patch = "\n".join(line.rstrip("\n") for line in diff_lines) counts = count_diff_lines(diff_lines) result["status"] = "modified" result["diff"] = { **counts, "patch": patch[:max_patch_chars], "patch_truncated": len(patch) > max_patch_chars, "patch_chars": len(patch), } return result def build_diff(workspace: Path, *, max_patch_chars: int) -> dict[str, Any]: integrity = check_workspace(workspace) if not integrity.get("passed"): return { "schema": "onec_patch_workspace_diff.v1", "workspace": str(workspace), "passed": False, "integrity": integrity, "files": [], "counts": { "files": 0, "modified": 0, "unchanged": 0, "missing": 0, "added_lines": 0, "removed_lines": 0, "hunks": 0, }, } manifest_path = workspace / "manifest.json" manifest = load_json(manifest_path) files = [diff_file(record, workspace=workspace, max_patch_chars=max_patch_chars) for record in manifest.get("files") or []] modified = [item for item in files if item.get("status") == "modified"] missing = [item for item in files if item.get("status") == "missing_file"] return { "schema": "onec_patch_workspace_diff.v1", "workspace": str(workspace), "manifest": str(manifest_path), "task": manifest.get("task"), "preferred_extension": manifest.get("preferred_extension"), "integrity": { "schema": integrity.get("schema"), "passed": integrity.get("passed"), "counts": integrity.get("counts"), }, "files": files, "counts": { "files": len(files), "modified": len(modified), "unchanged": len([item for item in files if item.get("status") == "unchanged"]), "missing": len(missing), "added_lines": sum((item.get("diff") or {}).get("added_lines", 0) for item in modified), "removed_lines": sum((item.get("diff") or {}).get("removed_lines", 0) for item in modified), "hunks": sum((item.get("diff") or {}).get("hunks", 0) for item in modified), }, "passed": not missing, } def main() -> int: parser = argparse.ArgumentParser(description="Diff 1C patch workspace original/working files.") parser.add_argument("--workspace", type=Path, required=True) parser.add_argument("--max-patch-chars", type=int, default=200000) parser.add_argument("--output", type=Path) args = parser.parse_args() result = build_diff(args.workspace, max_patch_chars=args.max_patch_chars) output = json.dumps(result, ensure_ascii=False, indent=2) + "\n" if args.output: args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(output, encoding="utf-8") print(json.dumps({"output": str(args.output) if args.output else None, "passed": result["passed"], "counts": result["counts"]}, ensure_ascii=False)) return 0 if result["passed"] else 2 if __name__ == "__main__": raise SystemExit(main())