intermediate commit
This commit is contained in:
137
tools/release/govoplan_release/repository_prepare.py
Normal file
137
tools/release/govoplan_release/repository_prepare.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user