| 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 | 
|---|