"""Prepare selected repositories by committing reviewed dirty worktrees.""" from __future__ import annotations from pathlib import Path import subprocess from .git_state import collect_repository_snapshot from .repository_push import command_text, compact_output from .workspace import load_repository_specs, resolve_repo_path, resolve_workspace_root def prepare_repositories( *, repos: tuple[str, ...], repo_versions: dict[str, str] | None = None, message: str | None = None, workspace_root: Path | str | None = None, apply: bool = False, # noqa: A002 - mirrors API field. ) -> dict[str, object]: workspace = resolve_workspace_root(workspace_root) specs = {spec.name: spec for spec in load_repository_specs(include_website=False)} selected = tuple(dict.fromkeys(repos)) versions = repo_versions or {} results: list[dict[str, object]] = [] for repo in selected: spec = specs.get(repo) if spec is None: results.append({"repo": repo, "status": "blocked", "detail": "repository is not listed in repositories.json"}) continue snapshot = collect_repository_snapshot(spec, workspace_root=workspace, target_tag=None, online=False) commit_message = resolve_message(repo=repo, version=versions.get(repo), message=message) command = f"{command_text(('git', 'add', '-A'))} && {command_text(('git', 'commit', '-m', commit_message))}" row: dict[str, object] = { "repo": repo, "cwd": snapshot.absolute_path, "branch": snapshot.branch, "upstream": snapshot.upstream, "head": snapshot.head, "ahead": int(snapshot.ahead or 0), "behind": int(snapshot.behind or 0), "dirty_count": len(snapshot.dirty_entries), "dirty_entries": snapshot.dirty_entries, "message": commit_message, "command": command, } if not snapshot.exists or not snapshot.is_git: results.append({**row, "status": "blocked", "detail": "repository is missing or is not a Git repository"}) continue if snapshot.safe_directory_required: results.append({**row, "status": "blocked", "detail": "Git safe.directory approval is required"}) continue if not snapshot.has_head: results.append({**row, "status": "blocked", "detail": "repository has no HEAD; initialize it outside the release prepare step"}) continue if not snapshot.branch: results.append({**row, "status": "blocked", "detail": "repository is not on a named branch"}) continue if snapshot.behind: results.append({**row, "status": "blocked", "detail": f"repository is behind {snapshot.upstream}; sync/update before committing"}) continue if not snapshot.dirty_entries: results.append({**row, "status": "noop", "detail": "worktree is clean; no prepare commit needed"}) continue if not apply: results.append({**row, "status": "planned", "detail": f"{len(snapshot.dirty_entries)} dirty/untracked path(s) will be committed"}) continue path = resolve_repo_path(spec, workspace) add_result = run(("git", "add", "-A"), cwd=path) if add_result.returncode != 0: results.append( { **row, "status": "failed", "returncode": add_result.returncode, "detail": "git add failed", "stdout": compact_output(add_result.stdout), "stderr": compact_output(add_result.stderr), } ) continue if run(("git", "diff", "--cached", "--quiet"), cwd=path).returncode == 0: results.append({**row, "status": "noop", "detail": "nothing staged after git add -A"}) continue commit_result = run(("git", "commit", "-m", commit_message), cwd=path) if commit_result.returncode != 0: results.append( { **row, "status": "failed", "returncode": commit_result.returncode, "detail": "git commit failed", "stdout": compact_output(commit_result.stdout), "stderr": compact_output(commit_result.stderr), } ) continue after = collect_repository_snapshot(spec, workspace_root=workspace, target_tag=None, online=False) results.append( { **row, "status": "committed", "returncode": commit_result.returncode, "detail": f"created commit {after.head or 'HEAD'}", "after_head": after.head, "after_ahead": int(after.ahead or 0), "after_dirty_count": len(after.dirty_entries), "stdout": compact_output(commit_result.stdout), "stderr": compact_output(commit_result.stderr), } ) if any(item["status"] in {"blocked", "failed"} for item in results): status = "blocked" if not apply else "partial" elif any(item["status"] == "committed" for item in results): status = "committed" elif any(item["status"] == "planned" for item in results): status = "planned" else: status = "noop" return {"status": status, "apply": apply, "repositories": results} def resolve_message(*, repo: str, version: str | None, message: str | None) -> str: if message and message.strip(): return message.strip() target = f"v{version.removeprefix('v')}" if version else "release" return f"Release {repo} {target}" def run(command: tuple[str, ...], *, cwd: Path) -> subprocess.CompletedProcess[str]: try: return subprocess.run(command, cwd=cwd, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120) except subprocess.TimeoutExpired as exc: return subprocess.CompletedProcess(command, 124, exc.stdout or "", exc.stderr or "git command timed out")