|
11 | 11 | import argparse
|
12 | 12 | from git import Repo # type: ignore
|
13 | 13 | import html
|
| 14 | +import json |
14 | 15 | import github
|
15 | 16 | import os
|
16 | 17 | import re
|
@@ -298,6 +299,76 @@ def run(self) -> bool:
|
298 | 299 | return True
|
299 | 300 |
|
300 | 301 |
|
| 302 | +class PRMergeOnBehalfInformation: |
| 303 | + COMMENT_TAG = "<!--LLVM MERGE ON BEHALF INFORMATION COMMENT-->\n" |
| 304 | + |
| 305 | + def __init__(self, token: str, repo: str, pr_number: int, author: str): |
| 306 | + repo = github.Github(token).get_repo(repo) |
| 307 | + self.pr = repo.get_issue(pr_number).as_pull_request() |
| 308 | + self.author = author |
| 309 | + self.repo = repo |
| 310 | + self.token = token |
| 311 | + |
| 312 | + def author_has_push_permission(self): |
| 313 | + # https://docs.github.com/en/rest/collaborators/collaborators?apiVersion=2022-11-28#get-repository-permissions-for-a-user |
| 314 | + response = requests.get( |
| 315 | + # Where repo is "owner/repo-name". |
| 316 | + f"https://api.github.com/repos/{self.repo}/collaborators/{self.author}/permission", |
| 317 | + headers={ |
| 318 | + "Accept": "application/vnd.github+json", |
| 319 | + "Authorization": f"Bearer {self.token}", |
| 320 | + "X-GitHub-Api-Version": "2022-11-28", |
| 321 | + }, |
| 322 | + ) |
| 323 | + |
| 324 | + # 404 means this user is not a collaborator. |
| 325 | + if response.status_code == 404: |
| 326 | + # Does not have push permission if not a collaborator. |
| 327 | + return False |
| 328 | + # User is a collaborator. |
| 329 | + elif response.status_code == 200: |
| 330 | + user_details = json.loads(response.text) |
| 331 | + user = user_details["user"] |
| 332 | + |
| 333 | + # We may have a list of permissions. |
| 334 | + if permissions := user.get("permissions"): |
| 335 | + return permissions["pull"] |
| 336 | + else: |
| 337 | + # Otherwise we can always fall back to the permission |
| 338 | + # on the top level object. The other permissions "read" and |
| 339 | + # "none" cannot push changes. |
| 340 | + return user_details["permisson"] in ["admin", "write"] |
| 341 | + else: |
| 342 | + # Something went wrong, log and carry on. |
| 343 | + print("Unexpected response code", response.status_code) |
| 344 | + # Assume they do have push permissions, so that we don't spam |
| 345 | + # PRs with comments if there are API problems. |
| 346 | + return True |
| 347 | + |
| 348 | + def run(self) -> bool: |
| 349 | + # A review can be approved more than once, only comment the first time. |
| 350 | + # Doing this check first as I'm assuming we get the comment data "free" in |
| 351 | + # terms of API cost. |
| 352 | + for comment in self.pr.as_issue().get_comments(): |
| 353 | + if self.COMMENT_TAG in comment.body: |
| 354 | + return |
| 355 | + |
| 356 | + # Now check whether the author has permissions needed to merge, which |
| 357 | + # uses a REST API call. |
| 358 | + if self.author_has_push_permission(): |
| 359 | + return |
| 360 | + |
| 361 | + # This text is using Markdown formatting. |
| 362 | + comment = f"""\ |
| 363 | +{self.COMMENT_TAG} |
| 364 | +@{self.author}, you do not have permissions to merge your own PRs yet. Please let us know when you are happy for this to be merged, and one of the reviewers can merge it on your behalf. |
| 365 | +
|
| 366 | +(if many approvals are required, please wait until everyone has approved before merging) |
| 367 | +""" |
| 368 | + self.pr.as_issue().create_comment(comment) |
| 369 | + return True |
| 370 | + |
| 371 | + |
301 | 372 | def setup_llvmbot_git(git_dir="."):
|
302 | 373 | """
|
303 | 374 | Configure the git repo in `git_dir` with the llvmbot account so
|
@@ -647,6 +718,14 @@ def execute_command(self) -> bool:
|
647 | 718 | pr_buildbot_information_parser.add_argument("--issue-number", type=int, required=True)
|
648 | 719 | pr_buildbot_information_parser.add_argument("--author", type=str, required=True)
|
649 | 720 |
|
| 721 | +pr_merge_on_behalf_information_parser = subparsers.add_parser( |
| 722 | + "pr-merge-on-behalf-information" |
| 723 | +) |
| 724 | +pr_merge_on_behalf_information_parser.add_argument( |
| 725 | + "--issue-number", type=int, required=True |
| 726 | +) |
| 727 | +pr_merge_on_behalf_information_parser.add_argument("--author", type=str, required=True) |
| 728 | + |
650 | 729 | release_workflow_parser = subparsers.add_parser("release-workflow")
|
651 | 730 | release_workflow_parser.add_argument(
|
652 | 731 | "--llvm-project-dir",
|
@@ -700,6 +779,11 @@ def execute_command(self) -> bool:
|
700 | 779 | args.token, args.repo, args.issue_number, args.author
|
701 | 780 | )
|
702 | 781 | pr_buildbot_information.run()
|
| 782 | +elif args.command == "pr-merge-on-behalf-information": |
| 783 | + pr_merge_on_behalf_information = PRMergeOnBehalfInformation( |
| 784 | + args.token, args.repo, args.issue_number, args.author |
| 785 | + ) |
| 786 | + pr_merge_on_behalf_information.run() |
703 | 787 | elif args.command == "release-workflow":
|
704 | 788 | release_workflow = ReleaseWorkflow(
|
705 | 789 | args.token,
|
|
0 commit comments