Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(262)

Side by Side Diff: gerrit_util.py

Issue 26399002: Add git/gerrit-on-borg utilities. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Simplify gerrit_util import Created 7 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | testing_support/gerrit-init.sh » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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 time
18 import urllib
19 from cStringIO import StringIO
20
21 try:
22 NETRC = netrc.netrc()
23 except (IOError, netrc.NetrcParseError):
24 NETRC = netrc.netrc(os.devnull)
25 LOGGER = logging.getLogger()
26 TRY_LIMIT = 5
27
28 # Controls the transport protocol used to communicate with gerrit.
29 # This is parameterized primarily to enable GerritTestCase.
30 GERRIT_PROTOCOL = 'https'
31
32
33 class GerritError(Exception):
34 """Exception class for errors commuicating with the gerrit-on-borg service."""
35 def __init__(self, http_status, *args, **kwargs):
36 super(GerritError, self).__init__(*args, **kwargs)
37 self.http_status = http_status
38 self.message = '(%d) %s' % (self.http_status, self.message)
39
40
41 def _QueryString(param_dict, first_param=None):
42 """Encodes query parameters in the key:val[+key:val...] format specified here:
43
44 https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#lis t-changes
45 """
46 q = [urllib.quote(first_param)] if first_param else []
47 q.extend(['%s:%s' % (key, val) for key, val in param_dict.iteritems()])
48 return '+'.join(q)
49
50
51 def GetConnectionClass(protocol=None):
52 if protocol is None:
53 protocol = GERRIT_PROTOCOL
54 if protocol == 'https':
55 return httplib.HTTPSConnection
56 elif protocol == 'http':
57 return httplib.HTTPConnection
58 else:
59 raise RuntimeError(
60 "Don't know how to work with protocol '%s'" % protocol)
61
62
63 def CreateHttpConn(host, path, reqtype='GET', headers=None, body=None):
64 """Opens an https connection to a gerrit service, and sends a request."""
65 headers = headers or {}
66 bare_host = host.partition(':')[0]
67 auth = NETRC.authenticators(bare_host)
68 if auth:
69 headers.setdefault('Authorization', 'Basic %s' % (
70 base64.b64encode('%s:%s' % (auth[0], auth[2]))))
71 else:
72 LOGGER.debug('No authorization found')
73 if body:
74 body = json.JSONEncoder().encode(body)
75 headers.setdefault('Content-Type', 'application/json')
76 if LOGGER.isEnabledFor(logging.DEBUG):
77 LOGGER.debug('%s %s://%s/a/%s' % (reqtype, GERRIT_PROTOCOL, host, path))
78 for key, val in headers.iteritems():
79 if key == 'Authorization':
80 val = 'HIDDEN'
81 LOGGER.debug('%s: %s' % (key, val))
82 if body:
83 LOGGER.debug(body)
84 conn = GetConnectionClass()(host)
85 conn.req_host = host
86 conn.req_params = {
87 'url': '/a/%s' % path,
88 'method': reqtype,
89 'headers': headers,
90 'body': body,
91 }
92 conn.request(**conn.req_params)
93 return conn
94
95
96 def ReadHttpResponse(conn, expect_status=200, ignore_404=True):
97 """Reads an http response from a connection into a string buffer.
98
99 Args:
100 conn: An HTTPSConnection or HTTPConnection created by CreateHttpConn, above.
101 expect_status: Success is indicated by this status in the response.
102 ignore_404: For many requests, gerrit-on-borg will return 404 if the request
103 doesn't match the database contents. In most such cases, we
104 want the API to return None rather than raise an Exception.
105 Returns: A string buffer containing the connection's reply.
106 """
107
108 sleep_time = 0.5
109 for idx in range(TRY_LIMIT):
110 response = conn.getresponse()
111 # If response.status < 500 then the result is final; break retry loop.
112 if response.status < 500:
113 break
114 # A status >=500 is assumed to be a possible transient error; retry.
115 http_version = 'HTTP/%s' % ('1.1' if response.version == 11 else '1.0')
116 msg = (
117 'A transient error occured while querying %s:\n'
118 '%s %s %s\n'
119 '%s %d %s' % (
120 conn.host, conn.req_params['method'], conn.req_params['url'],
121 http_version, http_version, response.status, response.reason))
122 if TRY_LIMIT - idx > 1:
123 msg += '\n... will retry %d more times.' % (TRY_LIMIT - idx - 1)
124 time.sleep(sleep_time)
125 sleep_time = sleep_time * 2
126 req_host = conn.req_host
127 req_params = conn.req_params
128 conn = GetConnectionClass()(req_host)
129 conn.req_host = req_host
130 conn.req_params = req_params
131 conn.request(**req_params)
132 LOGGER.warn(msg)
133 if ignore_404 and response.status == 404:
134 return StringIO()
135 if response.status != expect_status:
136 raise GerritError(response.status, response.reason)
137 return StringIO(response.read())
138
139
140 def ReadHttpJsonResponse(conn, expect_status=200, ignore_404=True):
141 """Parses an https response as json."""
142 fh = ReadHttpResponse(
143 conn, expect_status=expect_status, ignore_404=ignore_404)
144 # The first line of the response should always be: )]}'
145 s = fh.readline()
146 if s and s.rstrip() != ")]}'":
147 raise GerritError(200, 'Unexpected json output: %s' % s)
148 s = fh.read()
149 if not s:
150 return None
151 return json.loads(s)
152
153
154 def QueryChanges(host, param_dict, first_param=None, limit=None, o_params=None,
155 sortkey=None):
156 """
157 Queries a gerrit-on-borg server for changes matching query terms.
158
159 Args:
160 param_dict: A dictionary of search parameters, as documented here:
161 http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/user-se arch.html
162 first_param: A change identifier
163 limit: Maximum number of results to return.
164 o_params: A list of additional output specifiers, as documented here:
165 https://gerrit-review.googlesource.com/Documentation/rest-api-changes.ht ml#list-changes
166 Returns:
167 A list of json-decoded query results.
168 """
169 # Note that no attempt is made to escape special characters; YMMV.
170 if not param_dict and not first_param:
171 raise RuntimeError('QueryChanges requires search parameters')
172 path = 'changes/?q=%s' % _QueryString(param_dict, first_param)
173 if sortkey:
174 path = '%s&N=%s' % (path, sortkey)
175 if limit:
176 path = '%s&n=%d' % (path, limit)
177 if o_params:
178 path = '%s&%s' % (path, '&'.join(['o=%s' % p for p in o_params]))
179 # Don't ignore 404; a query should always return a list, even if it's empty.
180 return ReadHttpJsonResponse(CreateHttpConn(host, path), ignore_404=False)
181
182
183 def MultiQueryChanges(host, param_dict, change_list, limit=None, o_params=None,
184 sortkey=None):
185 """Initiate a query composed of multiple sets of query parameters."""
186 if not change_list:
187 raise RuntimeError(
188 "MultiQueryChanges requires a list of change numbers/id's")
189 q = ['q=%s' % '+OR+'.join([urllib.quote(str(x)) for x in change_list])]
190 if param_dict:
191 q.append(_QueryString(param_dict))
192 if limit:
193 q.append('n=%d' % limit)
194 if sortkey:
195 q.append('N=%s' % sortkey)
196 if o_params:
197 q.extend(['o=%s' % p for p in o_params])
198 path = 'changes/?%s' % '&'.join(q)
199 try:
200 result = ReadHttpJsonResponse(CreateHttpConn(host, path), ignore_404=False)
201 except GerritError as e:
202 msg = '%s:\n%s' % (e.message, path)
203 raise GerritError(e.http_status, msg)
204 return result
205
206
207 def GetGerritFetchUrl(host):
208 """Given a gerrit host name returns URL of a gerrit instance to fetch from."""
209 return '%s://%s/' % (GERRIT_PROTOCOL, host)
210
211
212 def GetChangePageUrl(host, change_number):
213 """Given a gerrit host name and change number, return change page url."""
214 return '%s://%s/#/c/%d/' % (GERRIT_PROTOCOL, host, change_number)
215
216
217 def GetChangeUrl(host, change):
218 """Given a gerrit host name and change id, return an url for the change."""
219 return '%s://%s/a/changes/%s' % (GERRIT_PROTOCOL, host, change)
220
221
222 def GetChange(host, change):
223 """Query a gerrit server for information about a single change."""
224 path = 'changes/%s' % change
225 return ReadHttpJsonResponse(CreateHttpConn(host, path))
226
227
228 def GetChangeDetail(host, change, o_params=None):
229 """Query a gerrit server for extended information about a single change."""
230 path = 'changes/%s/detail' % change
231 if o_params:
232 path += '?%s' % '&'.join(['o=%s' % p for p in o_params])
233 return ReadHttpJsonResponse(CreateHttpConn(host, path))
234
235
236 def GetChangeCurrentRevision(host, change):
237 """Get information about the latest revision for a given change."""
238 return QueryChanges(host, {}, change, o_params=('CURRENT_REVISION',))
239
240
241 def GetChangeRevisions(host, change):
242 """Get information about all revisions associated with a change."""
243 return QueryChanges(host, {}, change, o_params=('ALL_REVISIONS',))
244
245
246 def GetChangeReview(host, change, revision=None):
247 """Get the current review information for a change."""
248 if not revision:
249 jmsg = GetChangeRevisions(host, change)
250 if not jmsg:
251 return None
252 elif len(jmsg) > 1:
253 raise GerritError(200, 'Multiple changes found for ChangeId %s.' % change)
254 revision = jmsg[0]['current_revision']
255 path = 'changes/%s/revisions/%s/review'
256 return ReadHttpJsonResponse(CreateHttpConn(host, path))
257
258
259 def AbandonChange(host, change, msg=''):
260 """Abandon a gerrit change."""
261 path = 'changes/%s/abandon' % change
262 body = {'message': msg} if msg else None
263 conn = CreateHttpConn(host, path, reqtype='POST', body=body)
264 return ReadHttpJsonResponse(conn, ignore_404=False)
265
266
267 def RestoreChange(host, change, msg=''):
268 """Restore a previously abandoned change."""
269 path = 'changes/%s/restore' % change
270 body = {'message': msg} if msg else None
271 conn = CreateHttpConn(host, path, reqtype='POST', body=body)
272 return ReadHttpJsonResponse(conn, ignore_404=False)
273
274
275 def SubmitChange(host, change, wait_for_merge=True):
276 """Submits a gerrit change via Gerrit."""
277 path = 'changes/%s/submit' % change
278 body = {'wait_for_merge': wait_for_merge}
279 conn = CreateHttpConn(host, path, reqtype='POST', body=body)
280 return ReadHttpJsonResponse(conn, ignore_404=False)
281
282
283 def GetReviewers(host, change):
284 """Get information about all reviewers attached to a change."""
285 path = 'changes/%s/reviewers' % change
286 return ReadHttpJsonResponse(CreateHttpConn(host, path))
287
288
289 def GetReview(host, change, revision):
290 """Get review information about a specific revision of a change."""
291 path = 'changes/%s/revisions/%s/review' % (change, revision)
292 return ReadHttpJsonResponse(CreateHttpConn(host, path))
293
294
295 def AddReviewers(host, change, add=None):
296 """Add reviewers to a change."""
297 if not add:
298 return
299 if isinstance(add, basestring):
300 add = (add,)
301 path = 'changes/%s/reviewers' % change
302 for r in add:
303 body = {'reviewer': r}
304 conn = CreateHttpConn(host, path, reqtype='POST', body=body)
305 jmsg = ReadHttpJsonResponse(conn, ignore_404=False)
306 return jmsg
307
308
309 def RemoveReviewers(host, change, remove=None):
310 """Remove reveiewers from a change."""
311 if not remove:
312 return
313 if isinstance(remove, basestring):
314 remove = (remove,)
315 for r in remove:
316 path = 'changes/%s/reviewers/%s' % (change, r)
317 conn = CreateHttpConn(host, path, reqtype='DELETE')
318 try:
319 ReadHttpResponse(conn, ignore_404=False)
320 except GerritError as e:
321 # On success, gerrit returns status 204; anything else is an error.
322 if e.http_status != 204:
323 raise
324 else:
325 raise GerritError(
326 'Unexpectedly received a 200 http status while deleting reviewer "%s"'
327 ' from change %s' % (r, change))
328
329
330 def SetReview(host, change, msg=None, labels=None, notify=None):
331 """Set labels and/or add a message to a code review."""
332 if not msg and not labels:
333 return
334 path = 'changes/%s/revisions/current/review' % change
335 body = {}
336 if msg:
337 body['message'] = msg
338 if labels:
339 body['labels'] = labels
340 if notify:
341 body['notify'] = notify
342 conn = CreateHttpConn(host, path, reqtype='POST', body=body)
343 response = ReadHttpJsonResponse(conn)
344 if labels:
345 for key, val in labels.iteritems():
346 if ('labels' not in response or key not in response['labels'] or
347 int(response['labels'][key] != int(val))):
348 raise GerritError(200, 'Unable to set "%s" label on change %s.' % (
349 key, change))
350
351
352 def ResetReviewLabels(host, change, label, value='0', message=None,
353 notify=None):
354 """Reset the value of a given label for all reviewers on a change."""
355 # This is tricky, because we want to work on the "current revision", but
356 # there's always the risk that "current revision" will change in between
357 # API calls. So, we check "current revision" at the beginning and end; if
358 # it has changed, raise an exception.
359 jmsg = GetChangeCurrentRevision(host, change)
360 if not jmsg:
361 raise GerritError(
362 200, 'Could not get review information for change "%s"' % change)
363 value = str(value)
364 revision = jmsg[0]['current_revision']
365 path = 'changes/%s/revisions/%s/review' % (change, revision)
366 message = message or (
367 '%s label set to %s programmatically.' % (label, value))
368 jmsg = GetReview(host, change, revision)
369 if not jmsg:
370 raise GerritError(200, 'Could not get review information for revison %s '
371 'of change %s' % (revision, change))
372 for review in jmsg.get('labels', {}).get(label, {}).get('all', []):
373 if str(review.get('value', value)) != value:
374 body = {
375 'message': message,
376 'labels': {label: value},
377 'on_behalf_of': review['_account_id'],
378 }
379 if notify:
380 body['notify'] = notify
381 conn = CreateHttpConn(
382 host, path, reqtype='POST', body=body)
383 response = ReadHttpJsonResponse(conn)
384 if str(response['labels'][label]) != value:
385 username = review.get('email', jmsg.get('name', ''))
386 raise GerritError(200, 'Unable to set %s label for user "%s"'
387 ' on change %s.' % (label, username, change))
388 jmsg = GetChangeCurrentRevision(host, change)
389 if not jmsg:
390 raise GerritError(
391 200, 'Could not get review information for change "%s"' % change)
392 elif jmsg[0]['current_revision'] != revision:
393 raise GerritError(200, 'While resetting labels on change "%s", '
394 'a new patchset was uploaded.' % change)
OLDNEW
« no previous file with comments | « no previous file | testing_support/gerrit-init.sh » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698