Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
25 changes: 18 additions & 7 deletions github_rest_api/pr_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,15 @@ def deterministic_title(compare: dict[str, Any]) -> str:
"""Derive a Conventional-Commits title from a comparison result.

The type is the most significant type present across the commits
(``feat`` > ``fix`` > the most frequent parsed type > ``chore``); the scope
is the common top-level directory of the changed files; ``!`` is appended
for breaking changes.
(``feat`` > ``fix`` > the most frequent parsed type). When no commit follows
the Conventional-Commits grammar, the type is ``ci`` for changes confined to
``.github`` and ``chore`` otherwise. The scope is the common top-level
directory of the changed files; ``!`` is appended for breaking changes.

The description reuses a matching commit's description when available and
otherwise falls back to the first commit's subject line, so a genuinely
descriptive (if non-conventional) subject is preserved rather than replaced
by a generic placeholder.

:param compare: The comparison result from `Repository.compare`.
"""
Expand All @@ -94,17 +100,22 @@ def deterministic_title(compare: dict[str, Any]) -> str:
parsed = [parse_conventional(subject) for subject in subjects]
types = [item[0] for item in parsed if item]
scope = _common_scope(compare)
prefix_scope = f"({scope})" if scope else ""
if not subjects:
return f"chore{prefix_scope}: update"
if "feat" in types:
type_ = "feat"
elif "fix" in types:
type_ = "fix"
elif types:
type_ = Counter(types).most_common(1)[0][0]
elif scope == ".github":
# Changes confined to `.github` (e.g. workflow files) are conventionally
# `ci`; the type already conveys the location, so drop the (redundant)
# `.github` scope.
type_, scope = "ci", None
else:
type_ = "chore"
Comment thread
dclong marked this conversation as resolved.
prefix_scope = f"({scope})" if scope else ""
if not subjects:
return f"{type_}{prefix_scope}: update"
breaking = any(item[2] for item in parsed if item) or any(
_BREAKING_CHANGE_PATTERN.search(message) for message in messages
)
Expand All @@ -113,7 +124,7 @@ def deterministic_title(compare: dict[str, Any]) -> str:
else:
description = next(
(item[3] for item in parsed if item and item[0] == type_),
f"update {len(subjects)} commits",
subjects[0],
)
return f"{type_}{prefix_scope}{'!' if breaking else ''}: {description}"

Expand Down
48 changes: 48 additions & 0 deletions tests/test_pr_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,54 @@ def test_deterministic_title_no_scope_for_root_files():
assert deterministic_title(compare) == "chore: bump"


def test_deterministic_title_non_conventional_multi_uses_first_subject():
compare = {
"commits": [
_commit("1", "did some work"),
_commit("2", "did more work"),
],
"files": [_file("pkg/a.py"), _file("pkg/b.py")],
}
# No conventional commits: fall back to the first subject rather than the
# generic "update N commits" placeholder.
assert deterministic_title(compare) == "chore(pkg): did some work"


def test_deterministic_title_github_scope_maps_to_ci():
compare = {
"commits": [
_commit("1", "Claude PR Assistant workflow"),
_commit("2", "Claude Code Review workflow"),
],
"files": [
_file(".github/workflows/claude.yml", "added"),
_file(".github/workflows/claude-review.yml", "added"),
],
}
# `.github`-only changes become `ci` (with the redundant scope dropped), and
# the first subject supplies the description.
assert deterministic_title(compare) == "ci: Claude PR Assistant workflow"


def test_deterministic_title_github_scope_respects_conventional_type():
compare = {
"commits": [_commit("1", "docs: update issue template")],
"files": [_file(".github/ISSUE_TEMPLATE/bug.md")],
}
# An explicit conventional type wins over the `.github` -> `ci` fallback.
assert deterministic_title(compare) == "docs(.github): update issue template"


def test_deterministic_title_github_ci_not_applied_when_mixed_with_other_dir():
compare = {
"commits": [_commit("1", "tweak things")],
"files": [_file(".github/workflows/ci.yml"), _file("pkg/a.py")],
}
# Changes span `.github` and another top-level dir, so no common scope and no
# `ci` upgrade -- the generic `chore` fallback applies.
assert deterministic_title(compare) == "chore: tweak things"


def test_deterministic_body_sections():
body = deterministic_body(COMPARE)
assert "## Summary" in body
Expand Down