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
29 changes: 28 additions & 1 deletion changelog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
# Merge commits use a double linebreak between the branch name and the title
MERGE_PR_RE = re.compile(r"^Merge pull request #([0-9]+) from .*\n\n(.*)")

# Merge commits of a release branch
MERGE_RELEASE_PR_RE = re.compile(
r"^Merge pull request #([0-9]+) from .*/release/.*\n\n(.*)"
)

# Squash-and-merge commits use the PR title with the number in parentheses
SQUASH_PR_RE = re.compile(r"^(.*) \(#([0-9]+)\).*")

Expand Down Expand Up @@ -232,6 +237,11 @@ def is_pr(message):
return MERGE_PR_RE.search(message) or SQUASH_PR_RE.search(message)


def is_release_merge(message):
"""Determine whether or not a commit message is a release merge"""
return MERGE_RELEASE_PR_RE.search(message)


def extract_pr(message):
"""Given a PR merge commit message, extract the PR number and title"""
merge_match = MERGE_PR_RE.match(message)
Expand All @@ -254,6 +264,7 @@ def fetch_changes(
previous_tag=None,
current_tag=None,
branch=DEFAULT_BRANCH,
ignore_release_merge=False,
):
if previous_tag is None:
previous_tag = get_last_tag(github_config, owner, repo)
Expand Down Expand Up @@ -282,6 +293,9 @@ def fetch_changes(

# First try to extract PR from commit message (merge/squash patterns)
if is_pr(commit.message):
# Skip release merges if ignore_release_merge is True
if ignore_release_merge and is_release_merge(commit.message):
continue
pr = extract_pr(commit.message)
else:
# For rebase merges, try to find PR via GitHub API
Expand Down Expand Up @@ -356,13 +370,20 @@ def generate_changelog(
github_base_url=None,
github_api_url=None,
github_token=None,
ignore_release_merge=False,
):
github_config = get_github_config(
github_base_url, github_api_url, github_token
)

prs = fetch_changes(
github_config, owner, repo, previous_tag, current_tag, branch
github_config,
owner,
repo,
previous_tag,
current_tag,
branch,
ignore_release_merge,
)
lines = format_changes(github_config, owner, repo, prs, markdown=markdown)

Expand Down Expand Up @@ -435,6 +456,12 @@ def main():
help="GitHub oauth token to auth your Github requests with",
)

parser.add_argument(
"--ignore-release-merge",
action="store_true",
help="Override if you don't want to add release merges on the changelog",
)

args = parser.parse_args()

changelog = generate_changelog(**vars(args))
Expand Down
36 changes: 27 additions & 9 deletions changelog/tests/test_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ def test_get_commit_for_tag_exists(self, mock_requests_get):
"object": {"type": "commit", "sha": "0123456789abcdef"}
}
mock_requests_get.return_value = response
result = get_commit_for_tag(fake_github_config, "someone", "one-repo", "mytag")
result = get_commit_for_tag(
fake_github_config, "someone", "one-repo", "mytag"
)
self.assertEqual(result, "0123456789abcdef")

@mock.patch("requests.get")
Expand All @@ -78,7 +80,9 @@ def test_get_commit_for_tag_not_found(self, mock_requests_get):
response.json.return_value = {"message": "nope"}
mock_requests_get.return_value = response
with self.assertRaises(GitHubError):
get_commit_for_tag(fake_github_config, "someone", "one-repo", "mytag")
get_commit_for_tag(
fake_github_config, "someone", "one-repo", "mytag"
)

@mock.patch("requests.get")
def test_get_commit_for_tag_tag_object(self, mock_requests_get):
Expand All @@ -96,7 +100,9 @@ def test_get_commit_for_tag_tag_object(self, mock_requests_get):
{"object": {"type": "commit", "sha": "0123456789abcdef"}},
]
mock_requests_get.return_value = response
result = get_commit_for_tag(fake_github_config, "someone", "one-repo", "mytag")
result = get_commit_for_tag(
fake_github_config, "someone", "one-repo", "mytag"
)
self.assertEqual(result, "0123456789abcdef")

@mock.patch("requests.get")
Expand Down Expand Up @@ -158,7 +164,9 @@ def test_get_commits_between_no_commits(self, mock_requests_get):
response.json.return_value = {}
mock_requests_get.return_value = response
with self.assertRaises(GitHubError):
get_commits_between(fake_github_config, "someone", "one-repo", "one", "two")
get_commits_between(
fake_github_config, "someone", "one-repo", "one", "two"
)

@mock.patch("requests.get")
def test_get_commits_between_not_found(self, mock_requests_get):
Expand All @@ -168,7 +176,9 @@ def test_get_commits_between_not_found(self, mock_requests_get):
response.json.return_value = {"message": "nope"}
mock_requests_get.return_value = response
with self.assertRaises(GitHubError):
get_commits_between(fake_github_config, "someone", "one-repo", "one", "two")
get_commits_between(
fake_github_config, "someone", "one-repo", "one", "two"
)

@mock.patch("requests.get")
def test_get_pr_details(self, mock_requests_get):
Expand Down Expand Up @@ -271,7 +281,9 @@ def test_format_changes_uses_correct_base_url(self):
PullRequest(2, "second"), PullRequestDetails(None, None)
),
]
actual = format_changes(github_config, "owner", "a-repo", prs, markdown=True)
actual = format_changes(
github_config, "owner", "a-repo", prs, markdown=True
)
expected = [
"MINOR RELEASE",
"- first [#1](https://github.company.com/owner/a-repo/pull/1)",
Expand Down Expand Up @@ -395,13 +407,17 @@ def test_generate_changelog(self, mock_requests_get):
# For commit sha "8": "I made some changes!" - no PR associated
get_pr_for_commit_response_8 = mock.MagicMock()
get_pr_for_commit_response_8.status_code = 200
get_pr_for_commit_response_8.json.return_value = [] # Empty list means no PRs
get_pr_for_commit_response_8.json.return_value = (
[]
) # Empty list means no PRs
responses.append(get_pr_for_commit_response_8)

# For commit sha "7": malformed merge message - no PR associated
get_pr_for_commit_response_7 = mock.MagicMock()
get_pr_for_commit_response_7.status_code = 200
get_pr_for_commit_response_7.json.return_value = [] # Empty list means no PRs
get_pr_for_commit_response_7.json.return_value = (
[]
) # Empty list means no PRs
responses.append(get_pr_for_commit_response_7)

get_pr_details_response = mock.MagicMock()
Expand Down Expand Up @@ -475,7 +491,9 @@ def test_get_pr_for_commit(self, mock_requests_get):
self.assertEqual(result.title, "Add new feature")

# Verify the correct API endpoint was called
expected_url = "https://api.github.com/repos/owner/repo/commits/abc123/pulls"
expected_url = (
"https://api.github.com/repos/owner/repo/commits/abc123/pulls"
)
mock_requests_get.assert_called_with(expected_url, headers={})

@mock.patch("requests.get")
Expand Down