chromium/third_party/blink/tools/blinkpy/w3c/pr_cleanup_tool.py

"""Cleans up PRs that correspond to abandoned CLs in Gerrit."""

import logging
from datetime import datetime, timedelta, timezone
from typing import List, Optional

from blinkpy.w3c.gerrit import GerritAPI, GerritError, GerritNotFoundError
from blinkpy.w3c.wpt_github import PullRequest

_log = logging.getLogger(__name__)


class PrCleanupTool(object):
    def __init__(self, host):
        self.host = host

    def run(self, wpt_github, gerrit):
        """Closes all PRs that are abandoned two weeks ago in Gerrit."""
        _log.info(
            "Close exported PRs where the corresponding CLs have been abandoned."
        )
        pull_requests = self.retrieve_provisioned_prs(wpt_github)
        for pull_request in pull_requests:
            if pull_request.state != 'open':
                continue
            change_id = wpt_github.extract_metadata('Change-Id: ',
                                                    pull_request.body)
            maybe_cleanup_reason = self._cleanup_reason(gerrit, change_id)

            if maybe_cleanup_reason:
                self.log_affected_pr_details(wpt_github, pull_request,
                                             maybe_cleanup_reason)
                self.close_pr_and_delete_branch(wpt_github,
                                                pull_request.number,
                                                maybe_cleanup_reason)

        return True

    def _cleanup_reason(self, gerrit: GerritAPI,
                        change_id: Optional[str]) -> Optional[str]:
        """Get a human-readable comment describing why a PR will be closed.

        Returns `None` if the PR should not be closed.
        """
        if not change_id:
            return None
        try:
            cl = gerrit.query_cl(change_id)
        except GerritNotFoundError:
            return 'Close this PR because the corresponding CL has been deleted.'
        except GerritError as e:
            _log.error('Could not query change_id %s: %s', change_id, str(e))
            return None
        cl_status = cl.status
        expiration = datetime.now(timezone.utc) - timedelta(days=14)
        if cl_status == 'ABANDONED' and cl.updated < expiration:
            return 'Close this PR because the Chromium CL has been abandoned.'
        elif cl_status == 'MERGED' and not cl.is_exportable():
            return 'Close this PR because the Chromium CL does not have exportable changes.'
        return None

    def retrieve_provisioned_prs(self, wpt_github) -> List[PullRequest]:
        """Retrieves last 1000 PRs with 'do not merge' label."""
        return wpt_github.all_provisional_pull_requests()

    def close_pr_and_delete_branch(self, wpt_github, pull_request_number,
                                   comment):
        """Closes a PR with a comment and delete the corresponding branch."""
        wpt_github.add_comment(pull_request_number, comment)
        wpt_github.update_pr(pull_request_number, state='closed')
        branch = wpt_github.get_pr_branch(pull_request_number)
        wpt_github.delete_remote_branch(branch)

    def log_affected_pr_details(self, wpt_github, pull_request, comment):
        """Logs details of an affected PR."""
        _log.info(comment)
        _log.info('https://github.com/web-platform-tests/wpt/pull/%s',
                  pull_request.number)
        _log.info(
            wpt_github.extract_metadata('Reviewed-on: ', pull_request.body))