[Tizen] Add prelauncher
[platform/framework/web/crosswalk-tizen.git] / vendor / depot_tools / gerrit_util.py
1 # Copyright (c) 2013 The Chromium OS 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
5 """
6 Utilities for requesting information for a gerrit server via https.
7
8 https://gerrit-review.googlesource.com/Documentation/rest-api.html
9 """
10
11 import base64
12 import httplib
13 import json
14 import logging
15 import netrc
16 import os
17 import re
18 import stat
19 import sys
20 import time
21 import urllib
22 from cStringIO import StringIO
23
24 _netrc_file = '_netrc' if sys.platform.startswith('win') else '.netrc'
25 _netrc_file = os.path.join(os.environ['HOME'], _netrc_file)
26 try:
27   NETRC = netrc.netrc(_netrc_file)
28 except IOError:
29   print >> sys.stderr, 'WARNING: Could not read netrc file %s' % _netrc_file
30   NETRC = netrc.netrc(os.devnull)
31 except netrc.NetrcParseError as e:
32   _netrc_stat = os.stat(e.filename)
33   if _netrc_stat.st_mode & (stat.S_IRWXG | stat.S_IRWXO):
34     print >> sys.stderr, (
35         'WARNING: netrc file %s cannot be used because its file permissions '
36         'are insecure.  netrc file permissions should be 600.' % _netrc_file)
37   else:
38     print >> sys.stderr, ('ERROR: Cannot use netrc file %s due to a parsing '
39                           'error.' % _netrc_file)
40     raise
41   del _netrc_stat
42   NETRC = netrc.netrc(os.devnull)
43 del _netrc_file
44
45 LOGGER = logging.getLogger()
46 TRY_LIMIT = 5
47
48 # Controls the transport protocol used to communicate with gerrit.
49 # This is parameterized primarily to enable GerritTestCase.
50 GERRIT_PROTOCOL = 'https'
51
52
53 class GerritError(Exception):
54   """Exception class for errors commuicating with the gerrit-on-borg service."""
55   def __init__(self, http_status, *args, **kwargs):
56     super(GerritError, self).__init__(*args, **kwargs)
57     self.http_status = http_status
58     self.message = '(%d) %s' % (self.http_status, self.message)
59
60
61 class GerritAuthenticationError(GerritError):
62   """Exception class for authentication errors during Gerrit communication."""
63
64
65 def _QueryString(param_dict, first_param=None):
66   """Encodes query parameters in the key:val[+key:val...] format specified here:
67
68   https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes
69   """
70   q = [urllib.quote(first_param)] if first_param else []
71   q.extend(['%s:%s' % (key, val) for key, val in param_dict.iteritems()])
72   return '+'.join(q)
73
74
75 def GetConnectionClass(protocol=None):
76   if protocol is None:
77     protocol = GERRIT_PROTOCOL
78   if protocol == 'https':
79     return httplib.HTTPSConnection
80   elif protocol == 'http':
81     return httplib.HTTPConnection
82   else:
83     raise RuntimeError(
84         "Don't know how to work with protocol '%s'" % protocol)
85
86
87 def CreateHttpConn(host, path, reqtype='GET', headers=None, body=None):
88   """Opens an https connection to a gerrit service, and sends a request."""
89   headers = headers or {}
90   bare_host = host.partition(':')[0]
91   auth = NETRC.authenticators(bare_host)
92
93   if auth:
94     headers.setdefault('Authorization', 'Basic %s' % (
95         base64.b64encode('%s:%s' % (auth[0], auth[2]))))
96   else:
97     LOGGER.debug('No authorization found in netrc for %s.' % bare_host)
98
99   if 'Authorization' in headers and not path.startswith('a/'):
100     url = '/a/%s' % path
101   else:
102     url = '/%s' % path
103
104   if body:
105     body = json.JSONEncoder().encode(body)
106     headers.setdefault('Content-Type', 'application/json')
107   if LOGGER.isEnabledFor(logging.DEBUG):
108     LOGGER.debug('%s %s://%s%s' % (reqtype, GERRIT_PROTOCOL, host, url))
109     for key, val in headers.iteritems():
110       if key == 'Authorization':
111         val = 'HIDDEN'
112       LOGGER.debug('%s: %s' % (key, val))
113     if body:
114       LOGGER.debug(body)
115   conn = GetConnectionClass()(host)
116   conn.req_host = host
117   conn.req_params = {
118       'url': url,
119       'method': reqtype,
120       'headers': headers,
121       'body': body,
122   }
123   conn.request(**conn.req_params)
124   return conn
125
126
127 def ReadHttpResponse(conn, expect_status=200, ignore_404=True):
128   """Reads an http response from a connection into a string buffer.
129
130   Args:
131     conn: An HTTPSConnection or HTTPConnection created by CreateHttpConn, above.
132     expect_status: Success is indicated by this status in the response.
133     ignore_404: For many requests, gerrit-on-borg will return 404 if the request
134                 doesn't match the database contents.  In most such cases, we
135                 want the API to return None rather than raise an Exception.
136   Returns: A string buffer containing the connection's reply.
137   """
138
139   sleep_time = 0.5
140   for idx in range(TRY_LIMIT):
141     response = conn.getresponse()
142
143     # Check if this is an authentication issue.
144     www_authenticate = response.getheader('www-authenticate')
145     if (response.status in (httplib.UNAUTHORIZED, httplib.FOUND) and
146         www_authenticate):
147       auth_match = re.search('realm="([^"]+)"', www_authenticate, re.I)
148       host = auth_match.group(1) if auth_match else conn.req_host
149       reason = ('Authentication failed. Please make sure your .netrc file '
150                 'has credentials for %s' % host)
151       raise GerritAuthenticationError(response.status, reason)
152
153     # If response.status < 500 then the result is final; break retry loop.
154     if response.status < 500:
155       break
156     # A status >=500 is assumed to be a possible transient error; retry.
157     http_version = 'HTTP/%s' % ('1.1' if response.version == 11 else '1.0')
158     msg = (
159         'A transient error occured while querying %s:\n'
160         '%s %s %s\n'
161         '%s %d %s' % (
162             conn.host, conn.req_params['method'], conn.req_params['url'],
163             http_version, http_version, response.status, response.reason))
164     if TRY_LIMIT - idx > 1:
165       msg += '\n... will retry %d more times.' % (TRY_LIMIT - idx - 1)
166       time.sleep(sleep_time)
167       sleep_time = sleep_time * 2
168       req_host = conn.req_host
169       req_params = conn.req_params
170       conn = GetConnectionClass()(req_host)
171       conn.req_host = req_host
172       conn.req_params = req_params
173       conn.request(**req_params)
174     LOGGER.warn(msg)
175   if ignore_404 and response.status == 404:
176     return StringIO()
177   if response.status != expect_status:
178     reason = '%s: %s' % (response.reason, response.read())
179     raise GerritError(response.status, reason)
180   return StringIO(response.read())
181
182
183 def ReadHttpJsonResponse(conn, expect_status=200, ignore_404=True):
184   """Parses an https response as json."""
185   fh = ReadHttpResponse(
186       conn, expect_status=expect_status, ignore_404=ignore_404)
187   # The first line of the response should always be: )]}'
188   s = fh.readline()
189   if s and s.rstrip() != ")]}'":
190     raise GerritError(200, 'Unexpected json output: %s' % s)
191   s = fh.read()
192   if not s:
193     return None
194   return json.loads(s)
195
196
197 def QueryChanges(host, param_dict, first_param=None, limit=None, o_params=None,
198                  sortkey=None):
199   """
200   Queries a gerrit-on-borg server for changes matching query terms.
201
202   Args:
203     param_dict: A dictionary of search parameters, as documented here:
204         http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/user-search.html
205     first_param: A change identifier
206     limit: Maximum number of results to return.
207     o_params: A list of additional output specifiers, as documented here:
208         https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes
209   Returns:
210     A list of json-decoded query results.
211   """
212   # Note that no attempt is made to escape special characters; YMMV.
213   if not param_dict and not first_param:
214     raise RuntimeError('QueryChanges requires search parameters')
215   path = 'changes/?q=%s' % _QueryString(param_dict, first_param)
216   if sortkey:
217     path = '%s&N=%s' % (path, sortkey)
218   if limit:
219     path = '%s&n=%d' % (path, limit)
220   if o_params:
221     path = '%s&%s' % (path, '&'.join(['o=%s' % p for p in o_params]))
222   # Don't ignore 404; a query should always return a list, even if it's empty.
223   return ReadHttpJsonResponse(CreateHttpConn(host, path), ignore_404=False)
224
225
226 def GenerateAllChanges(host, param_dict, first_param=None, limit=500,
227                        o_params=None, sortkey=None):
228   """
229   Queries a gerrit-on-borg server for all the changes matching the query terms.
230
231   A single query to gerrit-on-borg is limited on the number of results by the
232   limit parameter on the request (see QueryChanges) and the server maximum
233   limit. This function uses the "_more_changes" and "_sortkey" attributes on
234   the returned changes to iterate all of them making multiple queries to the
235   server, regardless the query limit.
236
237   Args:
238     param_dict, first_param: Refer to QueryChanges().
239     limit: Maximum number of requested changes per query.
240     o_params: Refer to QueryChanges().
241     sortkey: The value of the "_sortkey" attribute where starts from. None to
242         start from the first change.
243
244   Returns:
245     A generator object to the list of returned changes, possibly unbound.
246   """
247   more_changes = True
248   while more_changes:
249     page = QueryChanges(host, param_dict, first_param, limit, o_params, sortkey)
250     for cl in page:
251       yield cl
252
253     more_changes = [cl for cl in page if '_more_changes' in cl]
254     if len(more_changes) > 1:
255       raise GerritError(
256           200,
257           'Received %d changes with a _more_changes attribute set but should '
258           'receive at most one.' % len(more_changes))
259     if more_changes:
260       sortkey = more_changes[0]['_sortkey']
261
262
263 def MultiQueryChanges(host, param_dict, change_list, limit=None, o_params=None,
264                       sortkey=None):
265   """Initiate a query composed of multiple sets of query parameters."""
266   if not change_list:
267     raise RuntimeError(
268         "MultiQueryChanges requires a list of change numbers/id's")
269   q = ['q=%s' % '+OR+'.join([urllib.quote(str(x)) for x in change_list])]
270   if param_dict:
271     q.append(_QueryString(param_dict))
272   if limit:
273     q.append('n=%d' % limit)
274   if sortkey:
275     q.append('N=%s' % sortkey)
276   if o_params:
277     q.extend(['o=%s' % p for p in o_params])
278   path = 'changes/?%s' % '&'.join(q)
279   try:
280     result = ReadHttpJsonResponse(CreateHttpConn(host, path), ignore_404=False)
281   except GerritError as e:
282     msg = '%s:\n%s' % (e.message, path)
283     raise GerritError(e.http_status, msg)
284   return result
285
286
287 def GetGerritFetchUrl(host):
288   """Given a gerrit host name returns URL of a gerrit instance to fetch from."""
289   return '%s://%s/' % (GERRIT_PROTOCOL, host)
290
291
292 def GetChangePageUrl(host, change_number):
293   """Given a gerrit host name and change number, return change page url."""
294   return '%s://%s/#/c/%d/' % (GERRIT_PROTOCOL, host, change_number)
295
296
297 def GetChangeUrl(host, change):
298   """Given a gerrit host name and change id, return an url for the change."""
299   return '%s://%s/a/changes/%s' % (GERRIT_PROTOCOL, host, change)
300
301
302 def GetChange(host, change):
303   """Query a gerrit server for information about a single change."""
304   path = 'changes/%s' % change
305   return ReadHttpJsonResponse(CreateHttpConn(host, path))
306
307
308 def GetChangeDetail(host, change, o_params=None):
309   """Query a gerrit server for extended information about a single change."""
310   path = 'changes/%s/detail' % change
311   if o_params:
312     path += '?%s' % '&'.join(['o=%s' % p for p in o_params])
313   return ReadHttpJsonResponse(CreateHttpConn(host, path))
314
315
316 def GetChangeCurrentRevision(host, change):
317   """Get information about the latest revision for a given change."""
318   return QueryChanges(host, {}, change, o_params=('CURRENT_REVISION',))
319
320
321 def GetChangeRevisions(host, change):
322   """Get information about all revisions associated with a change."""
323   return QueryChanges(host, {}, change, o_params=('ALL_REVISIONS',))
324
325
326 def GetChangeReview(host, change, revision=None):
327   """Get the current review information for a change."""
328   if not revision:
329     jmsg = GetChangeRevisions(host, change)
330     if not jmsg:
331       return None
332     elif len(jmsg) > 1:
333       raise GerritError(200, 'Multiple changes found for ChangeId %s.' % change)
334     revision = jmsg[0]['current_revision']
335   path = 'changes/%s/revisions/%s/review'
336   return ReadHttpJsonResponse(CreateHttpConn(host, path))
337
338
339 def AbandonChange(host, change, msg=''):
340   """Abandon a gerrit change."""
341   path = 'changes/%s/abandon' % change
342   body = {'message': msg} if msg else None
343   conn = CreateHttpConn(host, path, reqtype='POST', body=body)
344   return ReadHttpJsonResponse(conn, ignore_404=False)
345
346
347 def RestoreChange(host, change, msg=''):
348   """Restore a previously abandoned change."""
349   path = 'changes/%s/restore' % change
350   body = {'message': msg} if msg else None
351   conn = CreateHttpConn(host, path, reqtype='POST', body=body)
352   return ReadHttpJsonResponse(conn, ignore_404=False)
353
354
355 def SubmitChange(host, change, wait_for_merge=True):
356   """Submits a gerrit change via Gerrit."""
357   path = 'changes/%s/submit' % change
358   body = {'wait_for_merge': wait_for_merge}
359   conn = CreateHttpConn(host, path, reqtype='POST', body=body)
360   return ReadHttpJsonResponse(conn, ignore_404=False)
361
362
363 def GetReviewers(host, change):
364   """Get information about all reviewers attached to a change."""
365   path = 'changes/%s/reviewers' % change
366   return ReadHttpJsonResponse(CreateHttpConn(host, path))
367
368
369 def GetReview(host, change, revision):
370   """Get review information about a specific revision of a change."""
371   path = 'changes/%s/revisions/%s/review' % (change, revision)
372   return ReadHttpJsonResponse(CreateHttpConn(host, path))
373
374
375 def AddReviewers(host, change, add=None):
376   """Add reviewers to a change."""
377   if not add:
378     return
379   if isinstance(add, basestring):
380     add = (add,)
381   path = 'changes/%s/reviewers' % change
382   for r in add:
383     body = {'reviewer': r}
384     conn = CreateHttpConn(host, path, reqtype='POST', body=body)
385     jmsg = ReadHttpJsonResponse(conn, ignore_404=False)
386   return jmsg
387
388
389 def RemoveReviewers(host, change, remove=None):
390   """Remove reveiewers from a change."""
391   if not remove:
392     return
393   if isinstance(remove, basestring):
394     remove = (remove,)
395   for r in remove:
396     path = 'changes/%s/reviewers/%s' % (change, r)
397     conn = CreateHttpConn(host, path, reqtype='DELETE')
398     try:
399       ReadHttpResponse(conn, ignore_404=False)
400     except GerritError as e:
401       # On success, gerrit returns status 204; anything else is an error.
402       if e.http_status != 204:
403         raise
404     else:
405       raise GerritError(
406           'Unexpectedly received a 200 http status while deleting reviewer "%s"'
407           ' from change %s' % (r, change))
408
409
410 def SetReview(host, change, msg=None, labels=None, notify=None):
411   """Set labels and/or add a message to a code review."""
412   if not msg and not labels:
413     return
414   path = 'changes/%s/revisions/current/review' % change
415   body = {}
416   if msg:
417     body['message'] = msg
418   if labels:
419     body['labels'] = labels
420   if notify:
421     body['notify'] = notify
422   conn = CreateHttpConn(host, path, reqtype='POST', body=body)
423   response = ReadHttpJsonResponse(conn)
424   if labels:
425     for key, val in labels.iteritems():
426       if ('labels' not in response or key not in response['labels'] or
427           int(response['labels'][key] != int(val))):
428         raise GerritError(200, 'Unable to set "%s" label on change %s.' % (
429             key, change))
430
431
432 def ResetReviewLabels(host, change, label, value='0', message=None,
433                       notify=None):
434   """Reset the value of a given label for all reviewers on a change."""
435   # This is tricky, because we want to work on the "current revision", but
436   # there's always the risk that "current revision" will change in between
437   # API calls.  So, we check "current revision" at the beginning and end; if
438   # it has changed, raise an exception.
439   jmsg = GetChangeCurrentRevision(host, change)
440   if not jmsg:
441     raise GerritError(
442         200, 'Could not get review information for change "%s"' % change)
443   value = str(value)
444   revision = jmsg[0]['current_revision']
445   path = 'changes/%s/revisions/%s/review' % (change, revision)
446   message = message or (
447       '%s label set to %s programmatically.' % (label, value))
448   jmsg = GetReview(host, change, revision)
449   if not jmsg:
450     raise GerritError(200, 'Could not get review information for revison %s '
451                    'of change %s' % (revision, change))
452   for review in jmsg.get('labels', {}).get(label, {}).get('all', []):
453     if str(review.get('value', value)) != value:
454       body = {
455           'message': message,
456           'labels': {label: value},
457           'on_behalf_of': review['_account_id'],
458       }
459       if notify:
460         body['notify'] = notify
461       conn = CreateHttpConn(
462           host, path, reqtype='POST', body=body)
463       response = ReadHttpJsonResponse(conn)
464       if str(response['labels'][label]) != value:
465         username = review.get('email', jmsg.get('name', ''))
466         raise GerritError(200, 'Unable to set %s label for user "%s"'
467                        ' on change %s.' % (label, username, change))
468   jmsg = GetChangeCurrentRevision(host, change)
469   if not jmsg:
470     raise GerritError(
471         200, 'Could not get review information for change "%s"' % change)
472   elif jmsg[0]['current_revision'] != revision:
473     raise GerritError(200, 'While resetting labels on change "%s", '
474                    'a new patchset was uploaded.' % change)