Detect changes since last documentation generation. Returns a changes dict with affected modules, or None if no previous generation exists (first run). Detection strategy: 1. Git-based: compare stored commit_id with current HEAD, plus check uncommitted changes via ``git
(
repo_path: Path,
output_dir: Path,
)
| 26 | # --------------------------------------------------------------------------- |
| 27 | |
| 28 | |
| 29 | def _detect_changes( |
| 30 | repo_path: Path, |
| 31 | output_dir: Path, |
| 32 | ) -> dict[str, Any] | None: |
| 33 | """Detect changes since last documentation generation. |
| 34 | |
| 35 | Returns a changes dict with affected modules, or None if no previous |
| 36 | generation exists (first run). |
| 37 | |
| 38 | Detection strategy: |
| 39 | 1. Git-based: compare stored commit_id with current HEAD, plus check |
| 40 | uncommitted changes via ``git status``. |
| 41 | 2. Fallback: compare file mtime with stored ``timestamp`` in metadata. |
| 42 | """ |
| 43 | metadata_path = output_dir / "metadata.json" |
| 44 | module_tree_path = output_dir / "module_tree.json" |
| 45 | |
| 46 | if not metadata_path.exists() or not module_tree_path.exists(): |
| 47 | return None |
| 48 | |
| 49 | try: |
| 50 | metadata = json.loads(metadata_path.read_text(encoding="utf-8")) |
| 51 | module_tree = json.loads(module_tree_path.read_text(encoding="utf-8")) |
| 52 | except (json.JSONDecodeError, OSError, UnicodeDecodeError): |
| 53 | return None |
| 54 | |
| 55 | # Try git-based detection first |
| 56 | changes = _detect_via_git(repo_path, metadata, output_dir) |
| 57 | |
| 58 | # Fallback to mtime-based detection |
| 59 | if changes is None: |
| 60 | changes = _detect_via_mtime(repo_path, metadata) |
| 61 | |
| 62 | if changes is None: |
| 63 | return None |
| 64 | |
| 65 | changed_files = changes["changed_files"] |
| 66 | if not changed_files: |
| 67 | return { |
| 68 | "has_previous": True, |
| 69 | "no_changes": True, |
| 70 | "method": changes.get("method", "unknown"), |
| 71 | "message": "No changes detected since last generation. Documentation is up to date.", |
| 72 | } |
| 73 | |
| 74 | affected, cascade = _find_affected_modules(module_tree, changed_files) |
| 75 | |
| 76 | return { |
| 77 | "has_previous": True, |
| 78 | "no_changes": False, |
| 79 | "method": changes.get("method", "unknown"), |
| 80 | "changed_files": changed_files, |
| 81 | "affected_modules": sorted(affected), |
| 82 | "cascade_modules": sorted(cascade), |
| 83 | "hint": ( |
| 84 | f"Only {len(affected)} module(s) need updating: {sorted(affected)}. " |
| 85 | f"Parent modules to refresh: {sorted(cascade)}. " |
no test coverage detected