| Index: rietveld.py
|
| diff --git a/rietveld.py b/rietveld.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..6984d393423b7108070aa71b1743c9aab9fe8927
|
| --- /dev/null
|
| +++ b/rietveld.py
|
| @@ -0,0 +1,204 @@
|
| +# Copyright (c) 2011 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.
|
| +"""Defines class Rietveld to easily access a rietveld instance.
|
| +
|
| +Security implications:
|
| +
|
| +The following hypothesis are made:
|
| +- Rietveld enforces:
|
| + - Nobody else than issue owner can upload a patch set
|
| + - Verifies the issue owner credentials when creating new issues
|
| + - A issue owner can't change once the issue is created
|
| + - A patch set cannot be modified
|
| +"""
|
| +
|
| +import logging
|
| +import os
|
| +import sys
|
| +import time
|
| +import urllib2
|
| +
|
| +try:
|
| + import simplejson as json # pylint: disable=F0401
|
| +except ImportError:
|
| + try:
|
| + import json # pylint: disable=F0401
|
| + except ImportError:
|
| + # Import the one included in depot_tools.
|
| + sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
|
| + import simplejson as json # pylint: disable=F0401
|
| +
|
| +from third_party import upload
|
| +import patch
|
| +
|
| +# Hack out upload logging.info()
|
| +upload.logging = logging.getLogger('upload')
|
| +upload.logging.setLevel(logging.WARNING)
|
| +
|
| +
|
| +class Rietveld(object):
|
| + """Accesses rietveld."""
|
| + def __init__(self, url, email, password):
|
| + self.issue = None
|
| + self.user = email
|
| + self.url = url
|
| + self._get_creds = lambda: (email, password)
|
| + self._xsrf_token = None
|
| + self._xsrf_token_time = None
|
| + self.rpc_server = upload.HttpRpcServer(
|
| + self.url,
|
| + self._get_creds,
|
| + save_cookies=False)
|
| +
|
| + def xsrf_token(self):
|
| + if (not self._xsrf_token_time or
|
| + (time.time() - self._xsrf_token_time) > 30*60):
|
| + self._xsrf_token_time = time.time()
|
| + self._xsrf_token = self.get(
|
| + '/xsrf_token',
|
| + extra_headers={'X-Requesting-XSRF-Token': '1'})
|
| + return self._xsrf_token
|
| +
|
| + def get_pending_issues(self):
|
| + """Returns an array of dict of all the pending issues on the server."""
|
| + return json.loads(self.get(
|
| + '/search?format=json&commit=True&closed=False&keys_only=True')
|
| + )['results']
|
| +
|
| + def close_issue(self, issue):
|
| + """Closes the Rietveld issue for this changelist."""
|
| + logging.info('closing issue %s' % issue)
|
| + self.post("/%d/close" % issue, [('xsrf_token', self.xsrf_token())])
|
| +
|
| + def get_description(self, issue):
|
| + """Returns the issue's description."""
|
| + return self.get('/%d/description' % issue)
|
| +
|
| + def get_issue_properties(self, issue, messages):
|
| + """Returns all the issue's metadata as a dictionary."""
|
| + url = '/api/%s' % issue
|
| + if messages:
|
| + url += '?messages=true'
|
| + return json.loads(self.get(url))
|
| +
|
| + def get_patchset_properties(self, issue, patchset):
|
| + """Returns the patchset properties."""
|
| + url = '/api/%s/%s' % (issue, patchset)
|
| + return json.loads(self.get(url))
|
| +
|
| + def get_file_content(self, issue, patchset, item):
|
| + """Returns the content of a new file.
|
| +
|
| + Throws HTTP 302 exception if the file doesn't exist or is not a binary file.
|
| + """
|
| + # content = 0 is the old file, 1 is the new file.
|
| + content = 1
|
| + url = '/%s/image/%s/%s/%s' % (issue, patchset, item, content)
|
| + return self.get(url)
|
| +
|
| + def get_file_diff(self, issue, patchset, item):
|
| + """Returns the diff of the file.
|
| +
|
| + Returns a useless diff for binary files.
|
| + """
|
| + url = '/download/issue%s_%s_%s.diff' % (issue, patchset, item)
|
| + return self.get(url)
|
| +
|
| + def get_patch(self, issue, patchset):
|
| + """Returns a PatchSet object containing the details to apply this patch."""
|
| + props = self.get_patchset_properties(issue, patchset) or {}
|
| + out = []
|
| + for filename, state in props.get('files', {}).iteritems():
|
| + status = state.get('status')
|
| + if status is None:
|
| + raise patch.UnsupportedPatchFormat(
|
| + filename, 'File\'s status is None, patchset upload is incomplete')
|
| +
|
| + # TODO(maruel): That's bad, it confuses property change.
|
| + status = status.strip()
|
| +
|
| + if status == 'D':
|
| + # Ignore the diff.
|
| + out.append(patch.FilePatchDelete(filename, state['is_binary']))
|
| + elif status in ('A', 'M'):
|
| + # TODO(maruel): Rietveld support is still weird, add this line once it's
|
| + # safe to use.
|
| + # props = state.get('property_changes', '').splitlines() or []
|
| + props = []
|
| + if state['is_binary']:
|
| + out.append(patch.FilePatchBinary(
|
| + filename,
|
| + self.get_file_content(issue, patchset, state['id']),
|
| + props))
|
| + else:
|
| + if state['num_chunks']:
|
| + diff = self.get_file_diff(issue, patchset, state['id'])
|
| + else:
|
| + diff = None
|
| + out.append(patch.FilePatchDiff(filename, diff, props))
|
| + else:
|
| + # Line too long (N/80)
|
| + # pylint: disable=C0301
|
| + # TODO: Add support for MM, A+, etc. Rietveld removes the svn properties
|
| + # from the diff.
|
| + # Example of mergeinfo across branches:
|
| + # http://codereview.chromium.org/202046/diff/1/third_party/libxml/xmlcatalog_dummy.cc
|
| + # svn:eol-style property that is lost in the diff
|
| + # http://codereview.chromium.org/202046/diff/1/third_party/libxml/xmllint_dummy.cc
|
| + # Change with no diff, only svn property change:
|
| + # http://codereview.chromium.org/6462019/
|
| + raise patch.UnsupportedPatchFormat(filename, status)
|
| + return patch.PatchSet(out)
|
| +
|
| + def update_description(self, issue, description):
|
| + """Sets the description for an issue on Rietveld."""
|
| + logging.info('new description for issue %s' % issue)
|
| + self.post('/%s/description' % issue, [
|
| + ('description', description),
|
| + ('xsrf_token', self.xsrf_token())])
|
| +
|
| + def add_comment(self, issue, message):
|
| + logging.info('issue %s; comment: %s' % (issue, message))
|
| + return self.post('/%s/publish' % issue, [
|
| + ('xsrf_token', self.xsrf_token()),
|
| + ('message', message),
|
| + ('message_only', 'True'),
|
| + ('send_mail', 'True'),
|
| + ('no_redirect', 'True')])
|
| +
|
| + def set_flag(self, issue, patchset, flag, value):
|
| + return self.post('/%s/edit_flags' % issue, [
|
| + ('last_patchset', str(patchset)),
|
| + ('xsrf_token', self.xsrf_token()),
|
| + (flag, value)])
|
| +
|
| + def get(self, request_path, **kwargs):
|
| + return self._send(request_path, payload=None, **kwargs)
|
| +
|
| + def post(self, request_path, data, **kwargs):
|
| + ctype, body = upload.EncodeMultipartFormData(data, [])
|
| + return self._send(request_path, payload=body, content_type=ctype, **kwargs)
|
| +
|
| + def _send(self, request_path, **kwargs):
|
| + """Sends a POST/GET to Rietveld. Returns the response body."""
|
| + maxtries = 5
|
| + for retry in xrange(maxtries):
|
| + try:
|
| + result = self.rpc_server.Send(request_path, **kwargs)
|
| + # Sometimes GAE returns a HTTP 200 but with HTTP 500 as the content. How
|
| + # nice.
|
| + return result
|
| + except urllib2.HTTPError, e:
|
| + if retry >= (maxtries - 1):
|
| + raise
|
| + if e.code not in (500, 502, 503):
|
| + raise
|
| + except urllib2.URLError, e:
|
| + if retry >= (maxtries - 1):
|
| + raise
|
| + if not 'Name or service not known' in e.reason:
|
| + # Usually internal GAE flakiness.
|
| + raise
|
| + # If reaching this line, loop again. Uses a small backoff.
|
| + time.sleep(1+maxtries*2)
|
|
|