|  | # Copyright 2017 The Chromium Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | import base64 | 
|  | import json | 
|  | import logging | 
|  | from urllib2 import HTTPError | 
|  |  | 
|  | from blinkpy.common.net.network_transaction import NetworkTimeout | 
|  | from blinkpy.w3c.chromium_commit import ChromiumCommit | 
|  | from blinkpy.w3c.chromium_finder import absolute_chromium_dir | 
|  | from blinkpy.w3c.common import CHROMIUM_WPT_DIR, is_file_exportable | 
|  |  | 
|  | _log = logging.getLogger(__name__) | 
|  | URL_BASE = 'https://chromium-review.googlesource.com' | 
|  |  | 
|  |  | 
|  | class GerritAPI(object): | 
|  | """A utility class for the Chromium code review API. | 
|  |  | 
|  | Wraps the API for Chromium's Gerrit instance at chromium-review.googlesource.com. | 
|  | """ | 
|  |  | 
|  | def __init__(self, host, user, token): | 
|  | self.host = host | 
|  | self.user = user | 
|  | self.token = token | 
|  |  | 
|  | def get(self, path, raw=False): | 
|  | url = URL_BASE + path | 
|  | raw_data = self.host.web.get_binary(url) | 
|  | if raw: | 
|  | return raw_data | 
|  |  | 
|  | # Gerrit API responses are prefixed by a 5-character JSONP preamble | 
|  | return json.loads(raw_data[5:]) | 
|  |  | 
|  | def post(self, path, data): | 
|  | url = URL_BASE + path | 
|  | assert self.user and self.token, 'Gerrit user and token required for authenticated routes.' | 
|  |  | 
|  | b64auth = base64.b64encode('{}:{}'.format(self.user, self.token)) | 
|  | headers = { | 
|  | 'Authorization': 'Basic {}'.format(b64auth), | 
|  | 'Content-Type': 'application/json', | 
|  | } | 
|  | return self.host.web.request('POST', url, data=json.dumps(data), headers=headers) | 
|  |  | 
|  | def query_exportable_open_cls(self, limit=200): | 
|  | path = ('/changes/?q=project:\"chromium/src\"+status:open' | 
|  | '&o=CURRENT_FILES&o=CURRENT_REVISION&o=COMMIT_FOOTERS' | 
|  | '&o=DETAILED_ACCOUNTS&o=DETAILED_LABELS&n={}').format(limit) | 
|  | # The underlying host.web.get_binary() automatically retries until it | 
|  | # times out, at which point NetworkTimeout is raised. | 
|  | try: | 
|  | open_cls_data = self.get(path) | 
|  | except NetworkTimeout: | 
|  | raise GerritError('Timed out querying exportable open CLs.') | 
|  | open_cls = [GerritCL(data, self) for data in open_cls_data] | 
|  |  | 
|  | return [cl for cl in open_cls if cl.is_exportable()] | 
|  |  | 
|  |  | 
|  | class GerritCL(object): | 
|  | """A data wrapper for a Chromium Gerrit CL.""" | 
|  |  | 
|  | def __init__(self, data, api): | 
|  | assert data['change_id'] | 
|  | self._data = data | 
|  | self.api = api | 
|  |  | 
|  | @property | 
|  | def number(self): | 
|  | return self._data['_number'] | 
|  |  | 
|  | @property | 
|  | def url(self): | 
|  | return '{}/{}'.format(URL_BASE, self.number) | 
|  |  | 
|  | @property | 
|  | def subject(self): | 
|  | return self._data['subject'] | 
|  |  | 
|  | @property | 
|  | def change_id(self): | 
|  | return self._data['change_id'] | 
|  |  | 
|  | @property | 
|  | def owner_email(self): | 
|  | return self._data['owner']['email'] | 
|  |  | 
|  | @property | 
|  | def current_revision_sha(self): | 
|  | return self._data['current_revision'] | 
|  |  | 
|  | @property | 
|  | def current_revision(self): | 
|  | return self._data['revisions'][self.current_revision_sha] | 
|  |  | 
|  | @property | 
|  | def has_review_started(self): | 
|  | return self._data.get('has_review_started') | 
|  |  | 
|  | @property | 
|  | def current_revision_description(self): | 
|  | return self.current_revision['description'] | 
|  |  | 
|  | def post_comment(self, message): | 
|  | """Posts a comment to the CL.""" | 
|  | path = '/a/changes/{change_id}/revisions/current/review'.format( | 
|  | change_id=self.change_id, | 
|  | ) | 
|  | try: | 
|  | return self.api.post(path, {'message': message}) | 
|  | except HTTPError as e: | 
|  | raise GerritError('Failed to post a comment to issue {} (code {}).'.format(self.change_id, e.code)) | 
|  |  | 
|  | def is_exportable(self): | 
|  | # TODO(robertma): Consolidate with the related part in chromium_exportable_commits.py. | 
|  |  | 
|  | try: | 
|  | files = self.current_revision['files'].keys() | 
|  | except KeyError: | 
|  | # Empty (deleted) CL is not exportable. | 
|  | return False | 
|  |  | 
|  | # Guard against accidental CLs that touch thousands of files. | 
|  | if len(files) > 1000: | 
|  | _log.info('Rejecting CL with over 1000 files: %s (ID: %s) ', self.subject, self.change_id) | 
|  | return False | 
|  |  | 
|  | if self.subject.startswith('Import wpt@'): | 
|  | return False | 
|  |  | 
|  | if 'Import' in self.subject: | 
|  | return False | 
|  |  | 
|  | if 'No-Export: true' in self.current_revision['commit_with_footers']: | 
|  | return False | 
|  |  | 
|  | if 'NOEXPORT=true' in self.current_revision['commit_with_footers']: | 
|  | return False | 
|  |  | 
|  | files_in_wpt = [f for f in files if f.startswith(CHROMIUM_WPT_DIR)] | 
|  | if not files_in_wpt: | 
|  | return False | 
|  |  | 
|  | exportable_files = [f for f in files_in_wpt if is_file_exportable(f)] | 
|  |  | 
|  | if not exportable_files: | 
|  | return False | 
|  |  | 
|  | return True | 
|  |  | 
|  | def fetch_current_revision_commit(self, host): | 
|  | """Fetches the git commit for the latest revision of CL. | 
|  |  | 
|  | This method fetches the commit corresponding to the latest revision of | 
|  | CL to local Chromium repository, but does not checkout the commit to the | 
|  | working tree. All changes in the CL are squashed into this one commit, | 
|  | regardless of how many revisions have been uploaded. | 
|  |  | 
|  | Args: | 
|  | host: A Host object for git invocation. | 
|  |  | 
|  | Returns: | 
|  | A ChromiumCommit object (the fetched commit). | 
|  | """ | 
|  | git = host.git(absolute_chromium_dir(host)) | 
|  | url = self.current_revision['fetch']['http']['url'] | 
|  | ref = self.current_revision['fetch']['http']['ref'] | 
|  | git.run(['fetch', url, ref]) | 
|  | sha = git.run(['rev-parse', 'FETCH_HEAD']).strip() | 
|  | return ChromiumCommit(host, sha=sha) | 
|  |  | 
|  |  | 
|  | class GerritError(Exception): | 
|  | """Raised when Gerrit returns a non-OK response or times out.""" | 
|  | pass |