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

Side by Side Diff: presubmit_support.py

Issue 1927773002: Reland Implement owners check in presubmit for Gerrit. (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@P300
Patch Set: gerrit fix Created 4 years, 7 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
« no previous file with comments | « presubmit_canned_checks.py ('k') | tests/presubmit_unittest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Enables directory-specific presubmit checks to run at upload and/or commit. 6 """Enables directory-specific presubmit checks to run at upload and/or commit.
7 """ 7 """
8 8
9 __version__ = '1.8.0' 9 __version__ = '1.8.0'
10 10
(...skipping 179 matching lines...) Expand 10 before | Expand all | Expand 10 after
190 190
191 191
192 # Top level object so multiprocessing can pickle 192 # Top level object so multiprocessing can pickle
193 # Public access through OutputApi object. 193 # Public access through OutputApi object.
194 class _MailTextResult(_PresubmitResult): 194 class _MailTextResult(_PresubmitResult):
195 """A warning that should be included in the review request email.""" 195 """A warning that should be included in the review request email."""
196 def __init__(self, *args, **kwargs): 196 def __init__(self, *args, **kwargs):
197 super(_MailTextResult, self).__init__() 197 super(_MailTextResult, self).__init__()
198 raise NotImplementedError() 198 raise NotImplementedError()
199 199
200 class GerritAccessor(object):
201 """Limited Gerrit functionality for canned presubmit checks to work.
202
203 To avoid excessive Gerrit calls, caches the results.
204 """
205
206 def __init__(self, host):
207 self.host = host
208 self.cache = {}
209
210 def _FetchChangeDetail(self, issue):
211 # Separate function to be easily mocked in tests.
212 return gerrit_util.GetChangeDetail(
213 self.host, str(issue),
214 ['ALL_REVISIONS', 'DETAILED_LABELS'])
215
216 def GetChangeInfo(self, issue):
217 """Returns labels and all revisions (patchsets) for this issue.
218
219 The result is a dictionary according to Gerrit REST Api.
220 https://gerrit-review.googlesource.com/Documentation/rest-api.html
221
222 However, API isn't very clear what's inside, so see tests for example.
223 """
224 assert issue
225 cache_key = int(issue)
226 if cache_key not in self.cache:
227 self.cache[cache_key] = self._FetchChangeDetail(issue)
228 return self.cache[cache_key]
229
230 def GetChangeDescription(self, issue, patchset=None):
231 """If patchset is none, fetches current patchset."""
232 info = self.GetChangeInfo(issue)
233 # info is a reference to cache. We'll modify it here adding description to
234 # it to the right patchset, if it is not yet there.
235
236 # Find revision info for the patchset we want.
237 if patchset is not None:
238 for rev, rev_info in info['revisions'].iteritems():
239 if str(rev_info['_number']) == str(patchset):
240 break
241 else:
242 raise Exception('patchset %s doesn\'t exist in issue %s' % (
243 patchset, issue))
244 else:
245 rev = info['current_revision']
246 rev_info = info['revisions'][rev]
247
248 # Updates revision info, which is part of cached issue info.
249 if 'real_description' not in rev_info:
250 rev_info['real_description'] = (
251 gerrit_util.GetChangeDescriptionFromGitiles(
252 rev_info['fetch']['http']['url'], rev))
253 return rev_info['real_description']
254
255 def GetChangeOwner(self, issue):
256 return self.GetChangeInfo(issue)['owner']['email']
257
258 def GetChangeReviewers(self, issue, approving_only=True):
259 # Gerrit has 'approved' sub-section, but it only lists 1 approver.
260 # So, if we look only for approvers, we have to look at all anyway.
261 # Also, assume LGTM means Code-Review label == 2. Other configurations
262 # aren't supported.
263 return [r['email']
264 for r in self.GetChangeInfo(issue)['labels']['Code-Review']['all']
265 if not approving_only or '2' == str(r.get('value', 0))]
266
200 267
201 class OutputApi(object): 268 class OutputApi(object):
202 """An instance of OutputApi gets passed to presubmit scripts so that they 269 """An instance of OutputApi gets passed to presubmit scripts so that they
203 can output various types of results. 270 can output various types of results.
204 """ 271 """
205 PresubmitResult = _PresubmitResult 272 PresubmitResult = _PresubmitResult
206 PresubmitAddReviewers = _PresubmitAddReviewers 273 PresubmitAddReviewers = _PresubmitAddReviewers
207 PresubmitError = _PresubmitError 274 PresubmitError = _PresubmitError
208 PresubmitPromptWarning = _PresubmitPromptWarning 275 PresubmitPromptWarning = _PresubmitPromptWarning
209 PresubmitNotifyResult = _PresubmitNotifyResult 276 PresubmitNotifyResult = _PresubmitNotifyResult
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after
258 r".*\b[A-Z0-9_]{2,}$", 325 r".*\b[A-Z0-9_]{2,}$",
259 # SCM (can happen in dual SCM configuration). (Slightly over aggressive) 326 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
260 r"(|.*[\\\/])\.git[\\\/].*", 327 r"(|.*[\\\/])\.git[\\\/].*",
261 r"(|.*[\\\/])\.svn[\\\/].*", 328 r"(|.*[\\\/])\.svn[\\\/].*",
262 # There is no point in processing a patch file. 329 # There is no point in processing a patch file.
263 r".+\.diff$", 330 r".+\.diff$",
264 r".+\.patch$", 331 r".+\.patch$",
265 ) 332 )
266 333
267 def __init__(self, change, presubmit_path, is_committing, 334 def __init__(self, change, presubmit_path, is_committing,
268 rietveld_obj, verbose, dry_run=None): 335 rietveld_obj, verbose, gerrit_obj=None, dry_run=None):
269 """Builds an InputApi object. 336 """Builds an InputApi object.
270 337
271 Args: 338 Args:
272 change: A presubmit.Change object. 339 change: A presubmit.Change object.
273 presubmit_path: The path to the presubmit script being processed. 340 presubmit_path: The path to the presubmit script being processed.
274 is_committing: True if the change is about to be committed. 341 is_committing: True if the change is about to be committed.
275 rietveld_obj: rietveld.Rietveld client object 342 rietveld_obj: rietveld.Rietveld client object
343 gerrit_obj: provides basic Gerrit codereview functionality.
344 dry_run: if true, some Checks will be skipped.
276 """ 345 """
277 # Version number of the presubmit_support script. 346 # Version number of the presubmit_support script.
278 self.version = [int(x) for x in __version__.split('.')] 347 self.version = [int(x) for x in __version__.split('.')]
279 self.change = change 348 self.change = change
280 self.is_committing = is_committing 349 self.is_committing = is_committing
281 self.rietveld = rietveld_obj 350 self.rietveld = rietveld_obj
351 self.gerrit = gerrit_obj
282 self.dry_run = dry_run 352 self.dry_run = dry_run
283 # TBD 353 # TBD
284 self.host_url = 'http://codereview.chromium.org' 354 self.host_url = 'http://codereview.chromium.org'
285 if self.rietveld: 355 if self.rietveld:
286 self.host_url = self.rietveld.url 356 self.host_url = self.rietveld.url
287 357
288 # We expose various modules and functions as attributes of the input_api 358 # We expose various modules and functions as attributes of the input_api
289 # so that presubmit scripts don't have to import them. 359 # so that presubmit scripts don't have to import them.
290 self.basename = os.path.basename 360 self.basename = os.path.basename
291 self.cPickle = cPickle 361 self.cPickle = cPickle
(...skipping 1050 matching lines...) Expand 10 before | Expand all | Expand 10 after
1342 output_stream.write('** Post Upload Hook Messages **\n') 1412 output_stream.write('** Post Upload Hook Messages **\n')
1343 for result in results: 1413 for result in results:
1344 result.handle(output_stream) 1414 result.handle(output_stream)
1345 output_stream.write('\n') 1415 output_stream.write('\n')
1346 1416
1347 return results 1417 return results
1348 1418
1349 1419
1350 class PresubmitExecuter(object): 1420 class PresubmitExecuter(object):
1351 def __init__(self, change, committing, rietveld_obj, verbose, 1421 def __init__(self, change, committing, rietveld_obj, verbose,
1352 dry_run=None): 1422 gerrit_obj=None, dry_run=None):
1353 """ 1423 """
1354 Args: 1424 Args:
1355 change: The Change object. 1425 change: The Change object.
1356 committing: True if 'gcl commit' is running, False if 'gcl upload' is. 1426 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1357 rietveld_obj: rietveld.Rietveld client object. 1427 rietveld_obj: rietveld.Rietveld client object.
1428 gerrit_obj: provides basic Gerrit codereview functionality.
1429 dry_run: if true, some Checks will be skipped.
1358 """ 1430 """
1359 self.change = change 1431 self.change = change
1360 self.committing = committing 1432 self.committing = committing
1361 self.rietveld = rietveld_obj 1433 self.rietveld = rietveld_obj
1434 self.gerrit = gerrit_obj
1362 self.verbose = verbose 1435 self.verbose = verbose
1363 self.dry_run = dry_run 1436 self.dry_run = dry_run
1364 1437
1365 def ExecPresubmitScript(self, script_text, presubmit_path): 1438 def ExecPresubmitScript(self, script_text, presubmit_path):
1366 """Executes a single presubmit script. 1439 """Executes a single presubmit script.
1367 1440
1368 Args: 1441 Args:
1369 script_text: The text of the presubmit script. 1442 script_text: The text of the presubmit script.
1370 presubmit_path: The path to the presubmit file (this will be reported via 1443 presubmit_path: The path to the presubmit file (this will be reported via
1371 input_api.PresubmitLocalPath()). 1444 input_api.PresubmitLocalPath()).
1372 1445
1373 Return: 1446 Return:
1374 A list of result objects, empty if no problems. 1447 A list of result objects, empty if no problems.
1375 """ 1448 """
1376 1449
1377 # Change to the presubmit file's directory to support local imports. 1450 # Change to the presubmit file's directory to support local imports.
1378 main_path = os.getcwd() 1451 main_path = os.getcwd()
1379 os.chdir(os.path.dirname(presubmit_path)) 1452 os.chdir(os.path.dirname(presubmit_path))
1380 1453
1381 # Load the presubmit script into context. 1454 # Load the presubmit script into context.
1382 input_api = InputApi(self.change, presubmit_path, self.committing, 1455 input_api = InputApi(self.change, presubmit_path, self.committing,
1383 self.rietveld, self.verbose, 1456 self.rietveld, self.verbose,
1384 dry_run=self.dry_run) 1457 gerrit_obj=self.gerrit, dry_run=self.dry_run)
1385 context = {} 1458 context = {}
1386 try: 1459 try:
1387 exec script_text in context 1460 exec script_text in context
1388 except Exception, e: 1461 except Exception, e:
1389 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e)) 1462 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
1390 1463
1391 # These function names must change if we make substantial changes to 1464 # These function names must change if we make substantial changes to
1392 # the presubmit API that are not backwards compatible. 1465 # the presubmit API that are not backwards compatible.
1393 if self.committing: 1466 if self.committing:
1394 function_name = 'CheckChangeOnCommit' 1467 function_name = 'CheckChangeOnCommit'
(...skipping 22 matching lines...) Expand all
1417 1490
1418 1491
1419 def DoPresubmitChecks(change, 1492 def DoPresubmitChecks(change,
1420 committing, 1493 committing,
1421 verbose, 1494 verbose,
1422 output_stream, 1495 output_stream,
1423 input_stream, 1496 input_stream,
1424 default_presubmit, 1497 default_presubmit,
1425 may_prompt, 1498 may_prompt,
1426 rietveld_obj, 1499 rietveld_obj,
1500 gerrit_obj=None,
1427 dry_run=None): 1501 dry_run=None):
1428 """Runs all presubmit checks that apply to the files in the change. 1502 """Runs all presubmit checks that apply to the files in the change.
1429 1503
1430 This finds all PRESUBMIT.py files in directories enclosing the files in the 1504 This finds all PRESUBMIT.py files in directories enclosing the files in the
1431 change (up to the repository root) and calls the relevant entrypoint function 1505 change (up to the repository root) and calls the relevant entrypoint function
1432 depending on whether the change is being committed or uploaded. 1506 depending on whether the change is being committed or uploaded.
1433 1507
1434 Prints errors, warnings and notifications. Prompts the user for warnings 1508 Prints errors, warnings and notifications. Prompts the user for warnings
1435 when needed. 1509 when needed.
1436 1510
1437 Args: 1511 Args:
1438 change: The Change object. 1512 change: The Change object.
1439 committing: True if 'gcl commit' is running, False if 'gcl upload' is. 1513 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1440 verbose: Prints debug info. 1514 verbose: Prints debug info.
1441 output_stream: A stream to write output from presubmit tests to. 1515 output_stream: A stream to write output from presubmit tests to.
1442 input_stream: A stream to read input from the user. 1516 input_stream: A stream to read input from the user.
1443 default_presubmit: A default presubmit script to execute in any case. 1517 default_presubmit: A default presubmit script to execute in any case.
1444 may_prompt: Enable (y/n) questions on warning or error. 1518 may_prompt: Enable (y/n) questions on warning or error.
1445 rietveld_obj: rietveld.Rietveld object. 1519 rietveld_obj: rietveld.Rietveld object.
1520 gerrit_obj: provides basic Gerrit codereview functionality.
1446 dry_run: if true, some Checks will be skipped. 1521 dry_run: if true, some Checks will be skipped.
1447 1522
1448 Warning: 1523 Warning:
1449 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream 1524 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1450 SHOULD be sys.stdin. 1525 SHOULD be sys.stdin.
1451 1526
1452 Return: 1527 Return:
1453 A PresubmitOutput object. Use output.should_continue() to figure out 1528 A PresubmitOutput object. Use output.should_continue() to figure out
1454 if there were errors or warnings and the caller should abort. 1529 if there were errors or warnings and the caller should abort.
1455 """ 1530 """
1456 old_environ = os.environ 1531 old_environ = os.environ
1457 try: 1532 try:
1458 # Make sure python subprocesses won't generate .pyc files. 1533 # Make sure python subprocesses won't generate .pyc files.
1459 os.environ = os.environ.copy() 1534 os.environ = os.environ.copy()
1460 os.environ['PYTHONDONTWRITEBYTECODE'] = '1' 1535 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
1461 1536
1462 output = PresubmitOutput(input_stream, output_stream) 1537 output = PresubmitOutput(input_stream, output_stream)
1463 if committing: 1538 if committing:
1464 output.write("Running presubmit commit checks ...\n") 1539 output.write("Running presubmit commit checks ...\n")
1465 else: 1540 else:
1466 output.write("Running presubmit upload checks ...\n") 1541 output.write("Running presubmit upload checks ...\n")
1467 start_time = time.time() 1542 start_time = time.time()
1468 presubmit_files = ListRelevantPresubmitFiles( 1543 presubmit_files = ListRelevantPresubmitFiles(
1469 change.AbsoluteLocalPaths(True), change.RepositoryRoot()) 1544 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1470 if not presubmit_files and verbose: 1545 if not presubmit_files and verbose:
1471 output.write("Warning, no PRESUBMIT.py found.\n") 1546 output.write("Warning, no PRESUBMIT.py found.\n")
1472 results = [] 1547 results = []
1473 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose, 1548 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
1474 dry_run=dry_run) 1549 gerrit_obj, dry_run)
1475 if default_presubmit: 1550 if default_presubmit:
1476 if verbose: 1551 if verbose:
1477 output.write("Running default presubmit script.\n") 1552 output.write("Running default presubmit script.\n")
1478 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py') 1553 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1479 results += executer.ExecPresubmitScript(default_presubmit, fake_path) 1554 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1480 for filename in presubmit_files: 1555 for filename in presubmit_files:
1481 filename = os.path.abspath(filename) 1556 filename = os.path.abspath(filename)
1482 if verbose: 1557 if verbose:
1483 output.write("Running %s\n" % filename) 1558 output.write("Running %s\n" % filename)
1484 # Accept CRLF presubmit script. 1559 # Accept CRLF presubmit script.
(...skipping 204 matching lines...) Expand 10 before | Expand all | Expand 10 after
1689 "can be passed to this program.") 1764 "can be passed to this program.")
1690 if options.rietveld_email_file: 1765 if options.rietveld_email_file:
1691 with open(options.rietveld_email_file, "rb") as f: 1766 with open(options.rietveld_email_file, "rb") as f:
1692 options.rietveld_email = f.read().strip() 1767 options.rietveld_email = f.read().strip()
1693 1768
1694 change_class, files = load_files(options, args) 1769 change_class, files = load_files(options, args)
1695 if not change_class: 1770 if not change_class:
1696 parser.error('For unversioned directory, <files> is not optional.') 1771 parser.error('For unversioned directory, <files> is not optional.')
1697 logging.info('Found %d file(s).' % len(files)) 1772 logging.info('Found %d file(s).' % len(files))
1698 1773
1699 rietveld_obj = None 1774 rietveld_obj, gerrit_obj = None, None
1775
1700 if options.rietveld_url: 1776 if options.rietveld_url:
1701 # The empty password is permitted: '' is not None. 1777 # The empty password is permitted: '' is not None.
1702 if options.rietveld_private_key_file: 1778 if options.rietveld_private_key_file:
1703 rietveld_obj = rietveld.JwtOAuth2Rietveld( 1779 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1704 options.rietveld_url, 1780 options.rietveld_url,
1705 options.rietveld_email, 1781 options.rietveld_email,
1706 options.rietveld_private_key_file) 1782 options.rietveld_private_key_file)
1707 else: 1783 else:
1708 rietveld_obj = rietveld.CachingRietveld( 1784 rietveld_obj = rietveld.CachingRietveld(
1709 options.rietveld_url, 1785 options.rietveld_url,
1710 auth_config, 1786 auth_config,
1711 options.rietveld_email) 1787 options.rietveld_email)
1712 if options.rietveld_fetch: 1788 if options.rietveld_fetch:
1713 assert options.issue 1789 assert options.issue
1714 props = rietveld_obj.get_issue_properties(options.issue, False) 1790 props = rietveld_obj.get_issue_properties(options.issue, False)
1715 options.author = props['owner_email'] 1791 options.author = props['owner_email']
1716 options.description = props['description'] 1792 options.description = props['description']
1717 logging.info('Got author: "%s"', options.author) 1793 logging.info('Got author: "%s"', options.author)
1718 logging.info('Got description: """\n%s\n"""', options.description) 1794 logging.info('Got description: """\n%s\n"""', options.description)
1719 1795
1720 if options.gerrit_url and options.gerrit_fetch: 1796 if options.gerrit_url and options.gerrit_fetch:
1797 assert options.issue and options.patchset
1721 rietveld_obj = None 1798 rietveld_obj = None
1722 assert options.issue and options.patchset 1799 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1723 props = gerrit_util.GetChangeDetail( 1800 options.author = gerrit_obj.GetChangeOwner(options.issue)
1724 urlparse.urlparse(options.gerrit_url).netloc, str(options.issue), 1801 options.description = gerrit_obj.GetChangeDescription(options.issue,
1725 ['ALL_REVISIONS']) 1802 options.patchset)
1726 options.author = props['owner']['email']
1727 for rev, rev_info in props['revisions'].iteritems():
1728 if str(rev_info['_number']) == str(options.patchset):
1729 options.description = gerrit_util.GetChangeDescriptionFromGitiles(
1730 rev_info['fetch']['http']['url'], rev)
1731 break
1732 else:
1733 print >> sys.stderr, ('Patchset %d was not found in Gerrit issue %d' %
1734 options.patchset, options.issue)
1735 return 2
1736 logging.info('Got author: "%s"', options.author) 1803 logging.info('Got author: "%s"', options.author)
1737 logging.info('Got description: """\n%s\n"""', options.description) 1804 logging.info('Got description: """\n%s\n"""', options.description)
1738 1805
1739 try: 1806 try:
1740 with canned_check_filter(options.skip_canned): 1807 with canned_check_filter(options.skip_canned):
1741 results = DoPresubmitChecks( 1808 results = DoPresubmitChecks(
1742 change_class(options.name, 1809 change_class(options.name,
1743 options.description, 1810 options.description,
1744 options.root, 1811 options.root,
1745 files, 1812 files,
1746 options.issue, 1813 options.issue,
1747 options.patchset, 1814 options.patchset,
1748 options.author, 1815 options.author,
1749 upstream=options.upstream), 1816 upstream=options.upstream),
1750 options.commit, 1817 options.commit,
1751 options.verbose, 1818 options.verbose,
1752 sys.stdout, 1819 sys.stdout,
1753 sys.stdin, 1820 sys.stdin,
1754 options.default_presubmit, 1821 options.default_presubmit,
1755 options.may_prompt, 1822 options.may_prompt,
1756 rietveld_obj, 1823 rietveld_obj,
1824 gerrit_obj,
1757 options.dry_run) 1825 options.dry_run)
1758 return not results.should_continue() 1826 return not results.should_continue()
1759 except NonexistantCannedCheckFilter, e: 1827 except NonexistantCannedCheckFilter, e:
1760 print >> sys.stderr, ( 1828 print >> sys.stderr, (
1761 'Attempted to skip nonexistent canned presubmit check: %s' % e.message) 1829 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1762 return 2 1830 return 2
1763 except PresubmitFailure, e: 1831 except PresubmitFailure, e:
1764 print >> sys.stderr, e 1832 print >> sys.stderr, e
1765 print >> sys.stderr, 'Maybe your depot_tools is out of date?' 1833 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1766 print >> sys.stderr, 'If all fails, contact maruel@' 1834 print >> sys.stderr, 'If all fails, contact maruel@'
1767 return 2 1835 return 2
1768 1836
1769 1837
1770 if __name__ == '__main__': 1838 if __name__ == '__main__':
1771 fix_encoding.fix_encoding() 1839 fix_encoding.fix_encoding()
1772 try: 1840 try:
1773 sys.exit(main()) 1841 sys.exit(main())
1774 except KeyboardInterrupt: 1842 except KeyboardInterrupt:
1775 sys.stderr.write('interrupted\n') 1843 sys.stderr.write('interrupted\n')
1776 sys.exit(1) 1844 sys.exit(1)
OLDNEW
« no previous file with comments | « presubmit_canned_checks.py ('k') | tests/presubmit_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698