Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions repo_policy_sync/src/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,46 @@ def _pull_request_number(url: str) -> int:
return int(match.group(1))


def _tool_revision() -> str:
"""Return the current checkout's short commit hash and dirty marker."""

try:
revision_result = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
check=True,
capture_output=True,
text=True,
)
except FileNotFoundError as exc:
raise CommandError("required command is unavailable: git") from exc
except subprocess.CalledProcessError as exc:
detail = exc.stderr.strip() or exc.stdout.strip() or "command failed"
raise CommandError(f"git rev-parse --short HEAD: {detail}") from exc

revision = revision_result.stdout.strip()
if not revision:
raise CommandError("git rev-parse --short HEAD returned no commit hash")

try:
dirty_result = subprocess.run(
["git", "diff-index", "--quiet", "HEAD", "--"],
check=False,
capture_output=True,
text=True,
)
except FileNotFoundError as exc: # pragma: no cover - guarded by rev-parse
raise CommandError("required command is unavailable: git") from exc

if dirty_result.returncode not in (0, 1):
detail = (
dirty_result.stderr.strip()
or dirty_result.stdout.strip()
or "command failed"
)
raise CommandError(f"git diff-index --quiet HEAD --: {detail}")
return f"{revision}-dirty" if dirty_result.returncode == 1 else revision


def _pull_request_body(
policy: Policy,
changes: tuple[Change, ...],
Expand Down Expand Up @@ -815,6 +855,7 @@ def _pull_request_body(
"policy_description": description,
"policy_trigger": _policy_trigger(policy, changes),
"changes": change_lines,
"tool_revision": _tool_revision(),
Comment thread
AlexanderLanin marked this conversation as resolved.
Outdated
"failure_section": _failure_section(failure),
}
for key, value in values.items():
Expand Down
4 changes: 4 additions & 0 deletions repo_policy_sync/templates/pull_request.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@

{{ changes }}

## Tool revision

Generated from [eclipse-score/tools](https://github.com/eclipse-score/tools) at commit `{{ tool_revision }}`.

{{ failure_section }}

---
Expand Down
56 changes: 55 additions & 1 deletion repo_policy_sync/tests/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
GitHubCli,
PullRequest,
_pull_request_body,
_tool_revision,
policy_branches,
)
from repo_policy_sync.src.errors import CommandError, redact_sensitive_text
Expand Down Expand Up @@ -611,7 +612,10 @@ def run(command: list[str]) -> str:
assert commands[1][:4] == ["gh", "pr", "create", "--draft"]


def test_pull_request_template_explains_policy_trigger_and_changes() -> None:
def test_pull_request_template_explains_policy_trigger_and_changes(monkeypatch) -> None:
monkeypatch.setattr(
"repo_policy_sync.src.github._tool_revision", lambda: "abc1234-dirty"
)
policy = Policy(
"score-docs-as-code.cleanup",
"Update docs files",
Expand All @@ -633,12 +637,62 @@ def test_pull_request_template_explains_policy_trigger_and_changes() -> None:
)
assert "`MODULE.bazel` declares the required direct Bazel dependency" in body
assert "- `.gitignore`: add '_build'" in body
assert (
"Generated from [eclipse-score/tools](https://github.com/eclipse-score/tools) "
"at commit `abc1234-dirty`." in body
)
assert body.index("## Policy") < body.index("<!-- repo-policy-sync-policy:")
assert body.index("<!-- repo-policy-sync-policy:") < body.index(
"<!-- repo-policy-sync-head:"
)


def test_tool_revision_reports_a_clean_short_commit_hash(monkeypatch) -> None:
def run(command, **kwargs):
assert kwargs["capture_output"] is True
assert kwargs["text"] is True
if command == ["git", "rev-parse", "--short", "HEAD"]:
return subprocess.CompletedProcess(
command, 0, stdout="abc1234\n", stderr=""
)
if command == ["git", "diff-index", "--quiet", "HEAD", "--"]:
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
raise AssertionError(command)

monkeypatch.setattr("repo_policy_sync.src.github.subprocess.run", run)

assert _tool_revision() == "abc1234"


def test_tool_revision_marks_a_dirty_checkout(monkeypatch) -> None:
def run(command, **kwargs):
if command == ["git", "rev-parse", "--short", "HEAD"]:
return subprocess.CompletedProcess(
command, 0, stdout="abc1234\n", stderr=""
)
if command == ["git", "diff-index", "--quiet", "HEAD", "--"]:
return subprocess.CompletedProcess(command, 1, stdout="", stderr="")
raise AssertionError(command)

monkeypatch.setattr("repo_policy_sync.src.github.subprocess.run", run)

assert _tool_revision() == "abc1234-dirty"


def test_tool_revision_rejects_missing_git_metadata(monkeypatch) -> None:
def run(*_: object, **__: object) -> None:
raise subprocess.CalledProcessError(
128,
["git", "rev-parse", "--short", "HEAD"],
stderr="fatal: not a git repository\n",
)

monkeypatch.setattr("repo_policy_sync.src.github.subprocess.run", run)

with pytest.raises(CommandError, match="not a git repository"):
_tool_revision()


def test_module_policy_pull_request_includes_the_matching_rationale() -> None:
policy = load_policy(
BUNDLED_POLICY_DIRECTORY / "minimal-bazel-module-declaration" / "policy.yml"
Expand Down