blob: 9f4a023122e9556d506ba84e1e992ed868b34c3b [file] [log] [blame]
# 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_cl(self, change_id):
"""Quries a commit information from Gerrit."""
path = '/changes/%s' % (change_id)
try:
cl_data = self.get(path)
except NetworkTimeout:
raise GerritError('Timed out querying CL using changeid')
cl = GerritCL(data=cl_data, api=self)
return cl
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']
@property
def status(self):
return self._data['status']
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 '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