From a76a1ab2fda11ae982ce7233c2c31b5d2c771b59 Mon Sep 17 00:00:00 2001 From: Mariatta Wijaya Date: Mon, 10 Sep 2018 15:54:49 -0700 Subject: [PATCH] Automerge PR labeled with "automerge". Merge the PR if: - PR has "awaiting merge" and "automerge" labels, and - all CI status checks have passed, and - replace # with GH- - get the commit message from PR title and description Closes https://github.com/python/bedevere/issues/14 --- miss_islington/status_change.py | 92 ++++++----- miss_islington/util.py | 33 +++- tests/test_status_change.py | 279 +++++++++++++++++++++++++++++++- tests/test_util.py | 80 +++++++-- 4 files changed, 433 insertions(+), 51 deletions(-) diff --git a/miss_islington/status_change.py b/miss_islington/status_change.py index 7e285ef5..b253343d 100644 --- a/miss_islington/status_change.py +++ b/miss_islington/status_change.py @@ -14,23 +14,34 @@ async def check_status(event, gh, *args, **kwargs): """ Check the state change """ + sha = event.data["sha"] + if ( event.data["commit"].get("committer") and event.data["commit"]["committer"]["login"] == "miss-islington" ): - sha = event.data["sha"] await check_ci_status_and_approval(gh, sha, leave_comment=True) + else: + pr_for_commit = await util.get_pr_for_commit(gh, sha) + if pr_for_commit: + pr_labels = pr_for_commit["labels"] + if util.pr_is_automerge(pr_labels) and util.pr_is_awaiting_merge(pr_labels): + await check_ci_status_and_approval(gh, sha, leave_comment=True, is_automerge=True) @router.register("pull_request", action="labeled") async def pr_reviewed(event, gh, *args, **kwargs): - if event.data["pull_request"]["user"]["login"] == "miss-islington": - if util.pr_is_awaiting_merge(event.data["pull_request"]["labels"]): + + pr_labels = event.data["pull_request"]["labels"] + if util.pr_is_automerge(pr_labels) and util.pr_is_awaiting_merge(pr_labels): + sha = event.data["pull_request"]["head"]["sha"] + await check_ci_status_and_approval(gh, sha, is_automerge=True) + elif event.data["pull_request"]["user"]["login"] == "miss-islington" and util.pr_is_awaiting_merge(event.data["pull_request"]["labels"]): sha = event.data["pull_request"]["head"]["sha"] await check_ci_status_and_approval(gh, sha) -async def check_ci_status_and_approval(gh, sha, leave_comment=False): +async def check_ci_status_and_approval(gh, sha, leave_comment=False, is_automerge=False): result = await gh.getitem(f"/repos/python/cpython/commits/{sha}/status") all_ci_status = [status["state"] for status in result["statuses"]] @@ -40,56 +51,63 @@ async def check_ci_status_and_approval(gh, sha, leave_comment=False): "pending" not in all_ci_status and "continuous-integration/travis-ci/pr" in all_ci_context ): - - prs_for_commit = await gh.getitem(f'/search/issues?q=type:pr+repo:python/cpython+sha:{sha}') - if prs_for_commit["total_count"] > 0: # there should only be one - pr_for_commit = prs_for_commit["items"][0] + pr_for_commit = await util.get_pr_for_commit(gh, sha) + if pr_for_commit: pr_number = pr_for_commit["number"] normalized_pr_title = util.normalize_title( pr_for_commit["title"], pr_for_commit["body"] ) title_match = TITLE_RE.match(normalized_pr_title) - if title_match: + if title_match or is_automerge: if leave_comment: - original_pr_number = title_match.group("pr") - original_pr_url = ( - f"/repos/python/cpython/pulls/{original_pr_number}" - ) - original_pr_result = await gh.getitem(original_pr_url) - pr_author = original_pr_result["user"]["login"] - committer = original_pr_result["merged_by"]["login"] + if is_automerge: + participants = await util.get_participants(gh, pr_number) + else: + original_pr_number = title_match.group("pr") + participants = await util.get_participants(gh, original_pr_number) - participants = util.get_participants(pr_author, committer) emoji = "✅" if result["state"] == "success" else "❌" await util.leave_comment( gh, pr_number=pr_number, - message=f"{participants}: Backport status check is done, and it's a {result['state']} {emoji} .", + message=f"{participants}: Status check is done, and it's a {result['state']} {emoji} .", ) - if result["state"] == "success": - pr = await gh.getitem( - f"/repos/python/cpython/pulls/{pr_number}" - ) - if util.pr_is_awaiting_merge(pr["labels"]): - await merge_pr(gh, pr_number, sha) + if util.pr_is_awaiting_merge(pr_for_commit["labels"]): + print("awaiting merge") + await merge_pr(gh, pr_for_commit, sha, is_automerge=is_automerge) -async def merge_pr(gh, pr_number, sha): + +async def merge_pr(gh, pr, sha, is_automerge=False): + pr_number = pr["number"] async for commit in gh.getiter(f"/repos/python/cpython/pulls/{pr_number}/commits"): if commit["sha"] == sha: - pr_commit_msg = commit["commit"]["message"].split("\n") - - cleaned_up_title = f"{pr_commit_msg[0]}" - await gh.put( - f"/repos/python/cpython/pulls/{pr_number}/merge", - data={ - "commit_title": cleaned_up_title, - "commit_message": "\n".join(pr_commit_msg[1:]), - "sha": sha, - "merge_method": "squash", - }, - ) + if is_automerge: + pr_commit_msg = pr["body"] + pr_title = f"{pr['title']} (GH-{pr_number})" + await gh.put( + f"/repos/python/cpython/pulls/{pr_number}/merge", + data={ + "commit_title": pr_title, + "commit_message": pr_commit_msg, + "sha": sha, + "merge_method": "squash", + }, + ) + else: + pr_commit_msg = commit["commit"]["message"].split("\n") + + cleaned_up_title = f"{pr_commit_msg[0]}" + await gh.put( + f"/repos/python/cpython/pulls/{pr_number}/merge", + data={ + "commit_title": cleaned_up_title, + "commit_message": "\n".join(pr_commit_msg[1:]), + "sha": sha, + "merge_method": "squash", + }, + ) break diff --git a/miss_islington/util.py b/miss_islington/util.py index 8b0ad015..d41423da 100644 --- a/miss_islington/util.py +++ b/miss_islington/util.py @@ -66,12 +66,23 @@ def is_cpython_repo(): return True -def get_participants(created_by, merged_by): +async def get_participants(gh, pr_number): + pr_url = ( + f"/repos/python/cpython/pulls/{pr_number}" + ) + pr_result = await gh.getitem(pr_url) + created_by = pr_result["user"]["login"] + + merged_by = None + if pr_result["merged_by"]: + merged_by = pr_result["merged_by"]["login"] + participants = "" - if created_by == merged_by: + if created_by == merged_by or merged_by is None: participants = f"@{created_by}" - else: + elif merged_by is not None: participants = f"@{created_by} and @{merged_by}" + return participants @@ -113,3 +124,19 @@ def pr_is_awaiting_merge(pr_labels): if label["name"] == "awaiting merge": return True return False + + +def pr_is_automerge(pr_labels): + for label in pr_labels: + if label["name"] == "automerge": + return True + return False + + +async def get_pr_for_commit(gh, sha): + prs_for_commit = await gh.getitem( + f'/search/issues?q=type:pr+repo:python/cpython+sha:{sha}') + if prs_for_commit["total_count"] > 0: # there should only be one + pr_for_commit = prs_for_commit["items"][0] + return pr_for_commit + return None \ No newline at end of file diff --git a/tests/test_status_change.py b/tests/test_status_change.py index 8dff0fac..d065f7c6 100644 --- a/tests/test_status_change.py +++ b/tests/test_status_change.py @@ -71,6 +71,7 @@ async def test_ci_passed_with_awaiting_merge_label_pr_is_merged(): "number": 5547, "title": "[3.6] bpo-32720: Fixed the replacement field grammar documentation. (GH-5544)", "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.\n(cherry picked from commit 7a561afd2c79f63a6008843b83733911d07f0119)\n\nCo-authored-by: Mariatta ", + "labels": [{"name": "awaiting merge"}] } ], }, @@ -134,6 +135,7 @@ async def test_ci_passed_with_no_awaiting_merge_label_pr_is_not_merged(): "number": 5547, "title": "[3.6] bpo-32720: Fixed the replacement field grammar documentation. (GH-5544)", "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.\n(cherry picked from commit 7a561afd2c79f63a6008843b83733911d07f0119)\n\nCo-authored-by: Mariatta ", + "labels": [{"name": "awaiting core review"}] } ], }, @@ -179,6 +181,7 @@ async def test_ci_not_passed_awaiting_merge_label_pr_is_not_merged(): "number": 5547, "title": "[3.6] bpo-32720: Fixed the replacement field grammar documentation. (GH-5544)", "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.\n(cherry picked from commit 7a561afd2c79f63a6008843b83733911d07f0119)\n\nCo-authored-by: Mariatta ", + "labels": [{"name": "awaiting merge"}] } ], }, @@ -228,6 +231,7 @@ async def test_awaiting_merge_label_added_and_ci_passed_pr_is_merged(): "number": 5547, "title": "[3.6] bpo-32720: Fixed the replacement field grammar documentation. (GH-5544)", "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.\n(cherry picked from commit 7a561afd2c79f63a6008843b83733911d07f0119)\n\nCo-authored-by: Mariatta ", + "labels": [{"name": "awaiting merge"}] } ], }, @@ -293,6 +297,7 @@ async def test_awaiting_merge_webhook_ci_failure_pr_is_not_merged(): "number": 5547, "title": "[3.6] bpo-32720: Fixed the replacement field grammar documentation. (GH-5544)", "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.\n(cherry picked from commit 7a561afd2c79f63a6008843b83733911d07f0119)\n\nCo-authored-by: Mariatta ", + "labels": [{"name": "awaiting merge"}] } ], }, @@ -341,6 +346,7 @@ async def test_awaiting_core_review_label_added_is_not_merged(): "number": 5547, "title": "[3.6] bpo-32720: Fixed the replacement field grammar documentation. (GH-5544)", "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.\n(cherry picked from commit 7a561afd2c79f63a6008843b83733911d07f0119)\n\nCo-authored-by: Mariatta ", + "labels": [{"name": "awaiting merge"}] } ], }, @@ -417,6 +423,17 @@ async def test_ci_passed_with_awaiting_merge_label_not_miss_islington_is_not_mer "merged_by": {"login": "Mariatta"}, }, "/repos/python/cpython/pulls/5547": {"labels": [{"name": "awaiting merge"}]}, + f"/search/issues?q=type:pr+repo:python/cpython+sha:{sha}": { + "total_count": 1, + "items": [ + { + "number": 5547, + "title": "bpo-32720: Fixed the replacement field grammar documentation.", + "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.\n(cherry picked from commit 7a561afd2c79f63a6008843b83733911d07f0119)\n\nCo-authored-by: Mariatta ", + "labels": [{"name": "awaiting merge"}] + } + ], + }, } gh = FakeGH(getitem=getitem) @@ -542,6 +559,7 @@ async def test_pr_title_does_not_match(): "number": 5547, "title": "bpo-32720: Fixed the replacement field grammar documentation.", "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.\n(cherry picked from commit 7a561afd2c79f63a6008843b83733911d07f0119)\n\nCo-authored-by: Mariatta ", + "labels": [{"name": "awaiting merge"}] } ], }, @@ -600,6 +618,7 @@ async def test_ci_passed_awaiting_core_review_is_not_merged(): "number": 5547, "title": "[3.6] bpo-32720: Fixed the replacement field grammar documentation. (GH-5544)", "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.\n(cherry picked from commit 7a561afd2c79f63a6008843b83733911d07f0119)\n\nCo-authored-by: Mariatta ", + "labels": [{"name": "awaiting core review"}] } ], }, @@ -644,6 +663,7 @@ async def test_branch_sha_not_matched_pr_not_merged(): "number": 5547, "title": "[3.6] bpo-32720: Fixed the replacement field grammar documentation. (GH-5544)", "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.\n(cherry picked from commit 7a561afd2c79f63a6008843b83733911d07f0119)\n\nCo-authored-by: Mariatta ", + "labels": [{"name": "awaiting merge"}] } ], }, @@ -845,4 +865,261 @@ async def test_no_pr_containing_sha(): gh = FakeGH(getitem=getitem) await status_change.router.dispatch(event, gh) assert not hasattr(gh, "post_data") # does not leave any comment - assert not hasattr(gh, "put_data") # is not merged \ No newline at end of file + assert not hasattr(gh, "put_data") # is not merged + + +async def test_ci_passed_automerge(): + sha = "f2393593c99dd2d3ab8bfab6fcc5ddee540518a9" + data = {"sha": sha, "commit": {"committer": None, "author": None,}} + event = sansio.Event(data, event="status", delivery_id="1") + + getitem = { + f"/repos/python/cpython/commits/{sha}/status": { + "state": "success", + "statuses": [ + { + "state": "success", + "description": "Issue report skipped", + "context": "bedevere/issue-number", + }, + { + "state": "success", + "description": "The Travis CI build passed", + "target_url": "https://travis-ci.org/python/cpython/builds/340259685?utm_source=github_status&utm_medium=notification", + "context": "continuous-integration/travis-ci/pr", + }, + ], + }, + "/repos/python/cpython/pulls/5547": {"user": {"login": "bedevere-bot"}, + "merged_by": None, + "labels": [{"name": "awaiting merge"}]}, + f"/search/issues?q=type:pr+repo:python/cpython+sha:{sha}": { + "total_count": 1, + "items": [ + { + "number": 5547, + "title": "bpo-32720: Fixed the replacement field grammar documentation.", + "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.\n(cherry picked from commit 7a561afd2c79f63a6008843b83733911d07f0119)\n\nCo-authored-by: Mariatta ", + "labels": [{"name": "awaiting merge"}, {"name": "automerge"}] + } + ], + }, + } + + getiter = { + "/repos/python/cpython/pulls/5547/commits": [ + { + "sha": "f2393593c99dd2d3ab8bfab6fcc5ddee540518a9", + "commit": { + "message": "bpo-32720: Fixed the replacement field grammar documentation." + }, + } + ], + } + + gh = FakeGH(getitem=getitem, getiter=getiter) + await status_change.router.dispatch(event, gh) + assert len(gh.post_data["body"]) is not None # leaves a comment + assert gh.put_data["sha"] == sha # is merged + assert gh.put_data["merge_method"] == "squash" + assert ( + gh.put_data["commit_title"] + == "bpo-32720: Fixed the replacement field grammar documentation. (GH-5547)" + ) + + +async def test_ci_passed_not_automerge(): + sha = "f2393593c99dd2d3ab8bfab6fcc5ddee540518a9" + data = {"sha": sha, "commit": {"committer": None, "author": None,}} + event = sansio.Event(data, event="status", delivery_id="1") + + getitem = { + f"/repos/python/cpython/commits/{sha}/status": { + "state": "success", + "statuses": [ + { + "state": "success", + "description": "Issue report skipped", + "context": "bedevere/issue-number", + }, + { + "state": "success", + "description": "The Travis CI build passed", + "target_url": "https://travis-ci.org/python/cpython/builds/340259685?utm_source=github_status&utm_medium=notification", + "context": "continuous-integration/travis-ci/pr", + }, + ], + }, + "/repos/python/cpython/pulls/5547": {"user": {"login": "bedevere-bot"}, + "merged_by": None, + "labels": [{"name": "awaiting merge"}]}, + f"/search/issues?q=type:pr+repo:python/cpython+sha:{sha}": { + "total_count": 1, + "items": [ + { + "number": 5547, + "title": "bpo-32720: Fixed the replacement field grammar documentation.", + "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.", + "labels": [{"name": "awaiting merge"}] + } + ], + }, + } + + gh = FakeGH(getitem=getitem) + await status_change.router.dispatch(event, gh) + assert not hasattr(gh, "post_data") # does not leave a comment + assert not hasattr(gh, "put_data") # is not merged + + +async def test_awaiting_merge_label_and_automerge_label_added_not_miss_islingtons_pr(): + sha = "f2393593c99dd2d3ab8bfab6fcc5ddee540518a9" + data = { + "action": "labeled", + "pull_request": { + "user": {"login": "Mariatta"}, + "labels": [{"name": "awaiting merge"}, {"name": "automerge"}], + "head": {"sha": sha}, + }, + } + + event = sansio.Event(data, event="pull_request", delivery_id="1") + + getitem = { + f"/repos/python/cpython/commits/{sha}/status": { + "state": "success", + "statuses": [ + { + "state": "success", + "description": "Issue report skipped", + "context": "bedevere/issue-number", + }, + { + "state": "success", + "description": "The Travis CI build passed", + "target_url": "https://travis-ci.org/python/cpython/builds/340259685?utm_source=github_status&utm_medium=notification", + "context": "continuous-integration/travis-ci/pr", + }, + ], + }, + "/repos/python/cpython/pulls/5547": { + "user": {"login": "Mariatta"}, + "merged_by": None, + "labels": [{"name": "awaiting merge"}, {"name": "automerge"}], + }, + f"/search/issues?q=type:pr+repo:python/cpython+sha:{sha}": { + "total_count": 1, + "items": [ + { + "number": 5547, + "title": "bpo-32720: Fixed the replacement field grammar documentation.", + "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.", + "labels": [{"name": "awaiting merge"}, {"name": "automerge"}] + } + ], + } + } + + getiter = { + "/repos/python/cpython/pulls/5547/commits": [ + { + "sha": sha, + } + ], + } + + gh = FakeGH(getitem=getitem, getiter=getiter) + await status_change.router.dispatch(event, gh) + assert not hasattr(gh, "post_data") # does not leave a comment + + assert gh.put_data["sha"] == sha # is merged + assert gh.put_data["merge_method"] == "squash" + assert ( + gh.put_data["commit_title"] + == "bpo-32720: Fixed the replacement field grammar documentation. (GH-5547)" + ) + assert ( + gh.put_data["commit_message"] + == "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`." + ) + + +async def test_awaiting_automerge_but_not_awaiting_merge(): + sha = "f2393593c99dd2d3ab8bfab6fcc5ddee540518a9" + data = { + "action": "labeled", + "pull_request": { + "user": {"login": "Mariatta"}, + "labels": [{"name": "awaiting review"}, {"name": "automerge"}], + "head": {"sha": sha}, + }, + } + + event = sansio.Event(data, event="pull_request", delivery_id="1") + + getitem = { + f"/repos/python/cpython/commits/{sha}/status": { + "state": "success", + "statuses": [ + { + "state": "success", + "description": "Issue report skipped", + "context": "bedevere/issue-number", + }, + { + "state": "success", + "description": "The Travis CI build passed", + "target_url": "https://travis-ci.org/python/cpython/builds/340259685?utm_source=github_status&utm_medium=notification", + "context": "continuous-integration/travis-ci/pr", + }, + ], + }, + "/repos/python/cpython/pulls/5547": { + "user": {"login": "Mariatta"}, + "merged_by": None, + "labels": [{"name": "awaiting review"}, {"name": "automerge"}], + }, + f"/search/issues?q=type:pr+repo:python/cpython+sha:{sha}": { + "total_count": 1, + "items": [ + { + "number": 5547, + "title": "bpo-32720: Fixed the replacement field grammar documentation.", + "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.", + "labels": [{"name": "awaiting merge"}, {"name": "automerge"}] + } + ], + } + } + + getiter = { + "/repos/python/cpython/pulls/5547/commits": [ + { + "sha": sha, + } + ], + } + + gh = FakeGH(getitem=getitem, getiter=getiter) + await status_change.router.dispatch(event, gh) + assert not hasattr(gh, "post_data") # does not leave a comment + assert not hasattr(gh, "put_data") # does not leave a comment + + +async def test_pr_not_found_for_commit(): + sha = "f2393593c99dd2d3ab8bfab6fcc5ddee540518a9" + data = {"sha": sha, "commit": {"committer": None, "author": None,}} + + event = sansio.Event(data, event="status", delivery_id="1") + + getitem = { + f"/search/issues?q=type:pr+repo:python/cpython+sha:{sha}": { + "total_count": 0, + "items": [], + } + } + + gh = FakeGH(getitem=getitem) + await status_change.router.dispatch(event, gh) + assert not hasattr(gh, "post_data") # does not leave a comment + assert not hasattr(gh, "put_data") # does not leave a comment diff --git a/tests/test_util.py b/tests/test_util.py index fc6816e4..8b0bed8f 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -55,17 +55,34 @@ def test_title_normalization(): assert util.normalize_title(title, body) == expected -def test_get_participants_different_creator_and_committer(): - assert ( - util.get_participants("miss-islington", "bedevere-bot") - == "@miss-islington and @bedevere-bot" - ) +async def test_get_participants_different_creator_and_committer(): + gh = FakeGH(getitem={"/repos/python/cpython/pulls/5544": { + "user": {"login": "miss-islington"}, + "merged_by": {"login": "bedevere-bot"}, + }, + }) + result = await util.get_participants(gh, 5544) + assert result == "@miss-islington and @bedevere-bot" -def test_get_participants_same_creator_and_committer(): - assert ( - util.get_participants("miss-islington", "miss-islington") == "@miss-islington" - ) +async def test_get_participants_same_creator_and_committer(): + gh = FakeGH(getitem={"/repos/python/cpython/pulls/5544": { + "user": {"login": "miss-islington"}, + "merged_by": {"login": "miss-islington"}, + }, + }) + result = await util.get_participants(gh, 5544) + assert result == "@miss-islington" + + +async def test_get_participants_pr_not_merged(): + gh = FakeGH(getitem={"/repos/python/cpython/pulls/5544": { + "user": {"login": "miss-islington"}, + "merged_by": None, + }, + }) + result = await util.get_participants(gh, 5544) + assert result == "@miss-islington" @mock.patch("subprocess.check_output") @@ -120,11 +137,21 @@ def test_pr_is_awaiting_merge(): assert util.pr_is_awaiting_merge(labels) is True +def test_pr_is_automerge(): + labels = [{"name": "CLA Signed"}, {"name": "automerge"}, {"name": "awaiting review"}] + assert util.pr_is_automerge(labels) is True + + def test_pr_is_not_awaiting_merge(): - labels = [{"name": "CLA Signed", "name": "skip issue", "name": "awaiting review"}] + labels = [{"name": "CLA Signed"}, {"name": "skip issue"}, {"name": "awaiting review"}] assert util.pr_is_awaiting_merge(labels) is False +def test_pr_is_not_automerge(): + labels = [{"name": "CLA Signed"}, {"name": "awaiting merge"}] + assert util.pr_is_automerge(labels) is False + + def test_comment_on_pr_success(requests_mock): issue_number = 100 message = "Thanks for the PR!" @@ -170,3 +197,36 @@ def test_assign_pr_to_coredev_failed(requests_mock): requests_mock.patch(patch_url, status_code=400) response = util.assign_pr_to_core_dev(issue_number, coredev_login) assert response.status_code == 400 + + +async def test_get_pr_for_commit(): + sha = "f2393593c99dd2d3ab8bfab6fcc5ddee540518a9" + gh = FakeGH(getitem={f"/search/issues?q=type:pr+repo:python/cpython+sha:{sha}": { + "total_count": 1, + "items": [ + { + "number": 5547, + "title": "[3.6] bpo-32720: Fixed the replacement field grammar documentation. (GH-5544)", + "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.\n(cherry picked from commit 7a561afd2c79f63a6008843b83733911d07f0119)\n\nCo-authored-by: Mariatta ", + } + ], + },}) + result = await util.get_pr_for_commit(gh, sha) + assert result == { + "number": 5547, + "title": "[3.6] bpo-32720: Fixed the replacement field grammar documentation. (GH-5544)", + "body": "\n\n`arg_name` and `element_index` are defined as `digit`+ instead of `integer`.\n(cherry picked from commit 7a561afd2c79f63a6008843b83733911d07f0119)\n\nCo-authored-by: Mariatta ", + } + +async def test_get_pr_for_commit_not_found(): + sha = "f2393593c99dd2d3ab8bfab6fcc5ddee540518a9" + gh = FakeGH( + getitem={f"/search/issues?q=type:pr+repo:python/cpython+sha:{sha}": { + "total_count": 0, + "items": [ + + ], + }, }) + result = await util.get_pr_for_commit(gh, sha) + + assert result is None \ No newline at end of file