OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. |
| 4 """Defines class Rietveld to easily access a rietveld instance. |
| 5 |
| 6 Security implications: |
| 7 |
| 8 The following hypothesis are made: |
| 9 - Rietveld enforces: |
| 10 - Nobody else than issue owner can upload a patch set |
| 11 - Verifies the issue owner credentials when creating new issues |
| 12 - A issue owner can't change once the issue is created |
| 13 - A patch set cannot be modified |
| 14 """ |
| 15 |
| 16 import logging |
| 17 import os |
| 18 import sys |
| 19 import time |
| 20 import urllib2 |
| 21 |
| 22 try: |
| 23 import simplejson as json # pylint: disable=F0401 |
| 24 except ImportError: |
| 25 try: |
| 26 import json # pylint: disable=F0401 |
| 27 except ImportError: |
| 28 # Import the one included in depot_tools. |
| 29 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party')) |
| 30 import simplejson as json # pylint: disable=F0401 |
| 31 |
| 32 from third_party import upload |
| 33 import patch |
| 34 |
| 35 # Hack out upload logging.info() |
| 36 upload.logging = logging.getLogger('upload') |
| 37 upload.logging.setLevel(logging.WARNING) |
| 38 |
| 39 |
| 40 class Rietveld(object): |
| 41 """Accesses rietveld.""" |
| 42 def __init__(self, url, email, password): |
| 43 self.issue = None |
| 44 self.user = email |
| 45 self.url = url |
| 46 self._get_creds = lambda: (email, password) |
| 47 self._xsrf_token = None |
| 48 self._xsrf_token_time = None |
| 49 self.rpc_server = upload.HttpRpcServer( |
| 50 self.url, |
| 51 self._get_creds, |
| 52 save_cookies=False) |
| 53 |
| 54 def xsrf_token(self): |
| 55 if (not self._xsrf_token_time or |
| 56 (time.time() - self._xsrf_token_time) > 30*60): |
| 57 self._xsrf_token_time = time.time() |
| 58 self._xsrf_token = self.get( |
| 59 '/xsrf_token', |
| 60 extra_headers={'X-Requesting-XSRF-Token': '1'}) |
| 61 return self._xsrf_token |
| 62 |
| 63 def get_pending_issues(self): |
| 64 """Returns an array of dict of all the pending issues on the server.""" |
| 65 return json.loads(self.get( |
| 66 '/search?format=json&commit=True&closed=False&keys_only=True') |
| 67 )['results'] |
| 68 |
| 69 def close_issue(self, issue): |
| 70 """Closes the Rietveld issue for this changelist.""" |
| 71 logging.info('closing issue %s' % issue) |
| 72 self.post("/%d/close" % issue, [('xsrf_token', self.xsrf_token())]) |
| 73 |
| 74 def get_description(self, issue): |
| 75 """Returns the issue's description.""" |
| 76 return self.get('/%d/description' % issue) |
| 77 |
| 78 def get_issue_properties(self, issue, messages): |
| 79 """Returns all the issue's metadata as a dictionary.""" |
| 80 url = '/api/%s' % issue |
| 81 if messages: |
| 82 url += '?messages=true' |
| 83 return json.loads(self.get(url)) |
| 84 |
| 85 def get_patchset_properties(self, issue, patchset): |
| 86 """Returns the patchset properties.""" |
| 87 url = '/api/%s/%s' % (issue, patchset) |
| 88 return json.loads(self.get(url)) |
| 89 |
| 90 def get_file_content(self, issue, patchset, item): |
| 91 """Returns the content of a new file. |
| 92 |
| 93 Throws HTTP 302 exception if the file doesn't exist or is not a binary file. |
| 94 """ |
| 95 # content = 0 is the old file, 1 is the new file. |
| 96 content = 1 |
| 97 url = '/%s/image/%s/%s/%s' % (issue, patchset, item, content) |
| 98 return self.get(url) |
| 99 |
| 100 def get_file_diff(self, issue, patchset, item): |
| 101 """Returns the diff of the file. |
| 102 |
| 103 Returns a useless diff for binary files. |
| 104 """ |
| 105 url = '/download/issue%s_%s_%s.diff' % (issue, patchset, item) |
| 106 return self.get(url) |
| 107 |
| 108 def get_patch(self, issue, patchset): |
| 109 """Returns a PatchSet object containing the details to apply this patch.""" |
| 110 props = self.get_patchset_properties(issue, patchset) or {} |
| 111 out = [] |
| 112 for filename, state in props.get('files', {}).iteritems(): |
| 113 status = state.get('status') |
| 114 if status is None: |
| 115 raise patch.UnsupportedPatchFormat( |
| 116 filename, 'File\'s status is None, patchset upload is incomplete') |
| 117 |
| 118 # TODO(maruel): That's bad, it confuses property change. |
| 119 status = status.strip() |
| 120 |
| 121 if status == 'D': |
| 122 # Ignore the diff. |
| 123 out.append(patch.FilePatchDelete(filename, state['is_binary'])) |
| 124 elif status in ('A', 'M'): |
| 125 # TODO(maruel): Rietveld support is still weird, add this line once it's |
| 126 # safe to use. |
| 127 # props = state.get('property_changes', '').splitlines() or [] |
| 128 props = [] |
| 129 if state['is_binary']: |
| 130 out.append(patch.FilePatchBinary( |
| 131 filename, |
| 132 self.get_file_content(issue, patchset, state['id']), |
| 133 props)) |
| 134 else: |
| 135 if state['num_chunks']: |
| 136 diff = self.get_file_diff(issue, patchset, state['id']) |
| 137 else: |
| 138 diff = None |
| 139 out.append(patch.FilePatchDiff(filename, diff, props)) |
| 140 else: |
| 141 # Line too long (N/80) |
| 142 # pylint: disable=C0301 |
| 143 # TODO: Add support for MM, A+, etc. Rietveld removes the svn properties |
| 144 # from the diff. |
| 145 # Example of mergeinfo across branches: |
| 146 # http://codereview.chromium.org/202046/diff/1/third_party/libxml/xmlcat
alog_dummy.cc |
| 147 # svn:eol-style property that is lost in the diff |
| 148 # http://codereview.chromium.org/202046/diff/1/third_party/libxml/xmllin
t_dummy.cc |
| 149 # Change with no diff, only svn property change: |
| 150 # http://codereview.chromium.org/6462019/ |
| 151 raise patch.UnsupportedPatchFormat(filename, status) |
| 152 return patch.PatchSet(out) |
| 153 |
| 154 def update_description(self, issue, description): |
| 155 """Sets the description for an issue on Rietveld.""" |
| 156 logging.info('new description for issue %s' % issue) |
| 157 self.post('/%s/description' % issue, [ |
| 158 ('description', description), |
| 159 ('xsrf_token', self.xsrf_token())]) |
| 160 |
| 161 def add_comment(self, issue, message): |
| 162 logging.info('issue %s; comment: %s' % (issue, message)) |
| 163 return self.post('/%s/publish' % issue, [ |
| 164 ('xsrf_token', self.xsrf_token()), |
| 165 ('message', message), |
| 166 ('message_only', 'True'), |
| 167 ('send_mail', 'True'), |
| 168 ('no_redirect', 'True')]) |
| 169 |
| 170 def set_flag(self, issue, patchset, flag, value): |
| 171 return self.post('/%s/edit_flags' % issue, [ |
| 172 ('last_patchset', str(patchset)), |
| 173 ('xsrf_token', self.xsrf_token()), |
| 174 (flag, value)]) |
| 175 |
| 176 def get(self, request_path, **kwargs): |
| 177 return self._send(request_path, payload=None, **kwargs) |
| 178 |
| 179 def post(self, request_path, data, **kwargs): |
| 180 ctype, body = upload.EncodeMultipartFormData(data, []) |
| 181 return self._send(request_path, payload=body, content_type=ctype, **kwargs) |
| 182 |
| 183 def _send(self, request_path, **kwargs): |
| 184 """Sends a POST/GET to Rietveld. Returns the response body.""" |
| 185 maxtries = 5 |
| 186 for retry in xrange(maxtries): |
| 187 try: |
| 188 result = self.rpc_server.Send(request_path, **kwargs) |
| 189 # Sometimes GAE returns a HTTP 200 but with HTTP 500 as the content. How |
| 190 # nice. |
| 191 return result |
| 192 except urllib2.HTTPError, e: |
| 193 if retry >= (maxtries - 1): |
| 194 raise |
| 195 if e.code not in (500, 502, 503): |
| 196 raise |
| 197 except urllib2.URLError, e: |
| 198 if retry >= (maxtries - 1): |
| 199 raise |
| 200 if not 'Name or service not known' in e.reason: |
| 201 # Usually internal GAE flakiness. |
| 202 raise |
| 203 # If reaching this line, loop again. Uses a small backoff. |
| 204 time.sleep(1+maxtries*2) |
OLD | NEW |