blob: a9f80e272cf2bacb100ba1019ba30cb72012f679 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
maruel@chromium.org3bbf2942012-01-10 16:52:06 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Enables directory-specific presubmit checks to run at upload and/or commit.
7"""
8
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00009__version__ = '1.8.0'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000010
11# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
12# caching (between all different invocations of presubmit scripts for a given
13# change). We should add it as our presubmit scripts start feeling slow.
14
enne@chromium.orge72c5f52013-04-16 00:36:40 +000015import cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000016import cPickle # Exposed through the API.
17import cStringIO # Exposed through the API.
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +000018import contextlib
dcheng091b7db2016-06-16 01:27:51 -070019import fnmatch # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000020import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000021import inspect
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +000022import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000023import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000024import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000025import marshal # Exposed through the API.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000026import multiprocessing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000027import optparse
28import os # Somewhat exposed through the API.
29import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000030import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000031import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import sys # Parts exposed through API.
33import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000034import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000035import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000036import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000037import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000038import urllib2 # Exposed through the API.
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000039import urlparse
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000040from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000041
42# Local imports.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
maruel@chromium.org35625c72011-03-23 17:34:02 +000044import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000045import gclient_utils
Aaron Gableb584c4f2017-04-26 16:28:08 -070046import git_footers
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000047import gerrit_util
dpranke@chromium.org2a009622011-03-01 02:43:31 +000048import owners
Jochen Eisinger76f5fc62017-04-07 16:27:46 +020049import owners_finder
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000051import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000052import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000053import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000054
55
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000056# Ask for feedback only once in program lifetime.
57_ASKED_FOR_FEEDBACK = False
58
59
maruel@chromium.org899e1c12011-04-07 17:03:18 +000060class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000061 pass
62
63
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000064class CommandData(object):
65 def __init__(self, name, cmd, kwargs, message):
66 self.name = name
67 self.cmd = cmd
68 self.kwargs = kwargs
69 self.message = message
70 self.info = None
71
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000072
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000073def normpath(path):
74 '''Version of os.path.normpath that also changes backward slashes to
75 forward slashes when not running on Windows.
76 '''
77 # This is safe to always do because the Windows version of os.path.normpath
78 # will replace forward slashes with backward slashes.
79 path = path.replace(os.sep, '/')
80 return os.path.normpath(path)
81
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000082
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000083def _RightHandSideLinesImpl(affected_files):
84 """Implements RightHandSideLines for InputApi and GclChange."""
85 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000086 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000087 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000088 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000089
90
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000091class PresubmitOutput(object):
92 def __init__(self, input_stream=None, output_stream=None):
93 self.input_stream = input_stream
94 self.output_stream = output_stream
95 self.reviewers = []
96 self.written_output = []
97 self.error_count = 0
98
99 def prompt_yes_no(self, prompt_string):
100 self.write(prompt_string)
101 if self.input_stream:
102 response = self.input_stream.readline().strip().lower()
103 if response not in ('y', 'yes'):
104 self.fail()
105 else:
106 self.fail()
107
108 def fail(self):
109 self.error_count += 1
110
111 def should_continue(self):
112 return not self.error_count
113
114 def write(self, s):
115 self.written_output.append(s)
116 if self.output_stream:
117 self.output_stream.write(s)
118
119 def getvalue(self):
120 return ''.join(self.written_output)
121
122
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000123# Top level object so multiprocessing can pickle
124# Public access through OutputApi object.
125class _PresubmitResult(object):
126 """Base class for result objects."""
127 fatal = False
128 should_prompt = False
129
130 def __init__(self, message, items=None, long_text=''):
131 """
132 message: A short one-line message to indicate errors.
133 items: A list of short strings to indicate where errors occurred.
134 long_text: multi-line text output, e.g. from another tool
135 """
136 self._message = message
137 self._items = items or []
138 if items:
139 self._items = items
140 self._long_text = long_text.rstrip()
141
142 def handle(self, output):
143 output.write(self._message)
144 output.write('\n')
145 for index, item in enumerate(self._items):
146 output.write(' ')
147 # Write separately in case it's unicode.
148 output.write(str(item))
149 if index < len(self._items) - 1:
150 output.write(' \\')
151 output.write('\n')
152 if self._long_text:
153 output.write('\n***************\n')
154 # Write separately in case it's unicode.
155 output.write(self._long_text)
156 output.write('\n***************\n')
157 if self.fatal:
158 output.fail()
159
160
161# Top level object so multiprocessing can pickle
162# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000163class _PresubmitError(_PresubmitResult):
164 """A hard presubmit error."""
165 fatal = True
166
167
168# Top level object so multiprocessing can pickle
169# Public access through OutputApi object.
170class _PresubmitPromptWarning(_PresubmitResult):
171 """An warning that prompts the user if they want to continue."""
172 should_prompt = True
173
174
175# Top level object so multiprocessing can pickle
176# Public access through OutputApi object.
177class _PresubmitNotifyResult(_PresubmitResult):
178 """Just print something to the screen -- but it's not even a warning."""
179 pass
180
181
182# Top level object so multiprocessing can pickle
183# Public access through OutputApi object.
184class _MailTextResult(_PresubmitResult):
185 """A warning that should be included in the review request email."""
186 def __init__(self, *args, **kwargs):
187 super(_MailTextResult, self).__init__()
188 raise NotImplementedError()
189
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000190class GerritAccessor(object):
191 """Limited Gerrit functionality for canned presubmit checks to work.
192
193 To avoid excessive Gerrit calls, caches the results.
194 """
195
196 def __init__(self, host):
197 self.host = host
198 self.cache = {}
199
200 def _FetchChangeDetail(self, issue):
201 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100202 try:
203 return gerrit_util.GetChangeDetail(
204 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700205 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100206 except gerrit_util.GerritError as e:
207 if e.http_status == 404:
208 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
209 'no credentials to fetch issue details' % issue)
210 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000211
212 def GetChangeInfo(self, issue):
213 """Returns labels and all revisions (patchsets) for this issue.
214
215 The result is a dictionary according to Gerrit REST Api.
216 https://gerrit-review.googlesource.com/Documentation/rest-api.html
217
218 However, API isn't very clear what's inside, so see tests for example.
219 """
220 assert issue
221 cache_key = int(issue)
222 if cache_key not in self.cache:
223 self.cache[cache_key] = self._FetchChangeDetail(issue)
224 return self.cache[cache_key]
225
226 def GetChangeDescription(self, issue, patchset=None):
227 """If patchset is none, fetches current patchset."""
228 info = self.GetChangeInfo(issue)
229 # info is a reference to cache. We'll modify it here adding description to
230 # it to the right patchset, if it is not yet there.
231
232 # Find revision info for the patchset we want.
233 if patchset is not None:
234 for rev, rev_info in info['revisions'].iteritems():
235 if str(rev_info['_number']) == str(patchset):
236 break
237 else:
238 raise Exception('patchset %s doesn\'t exist in issue %s' % (
239 patchset, issue))
240 else:
241 rev = info['current_revision']
242 rev_info = info['revisions'][rev]
243
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100244 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000245
246 def GetChangeOwner(self, issue):
247 return self.GetChangeInfo(issue)['owner']['email']
248
249 def GetChangeReviewers(self, issue, approving_only=True):
Aaron Gable8b478f02017-07-31 15:33:19 -0700250 changeinfo = self.GetChangeInfo(issue)
251 if approving_only:
252 labelinfo = changeinfo.get('labels', {}).get('Code-Review', {})
253 values = labelinfo.get('values', {}).keys()
254 try:
255 max_value = max(int(v) for v in values)
256 reviewers = [r for r in labelinfo.get('all', [])
257 if r.get('value', 0) == max_value]
258 except ValueError: # values is the empty list
259 reviewers = []
260 else:
261 reviewers = changeinfo.get('reviewers', {}).get('REVIEWER', [])
262 return [r.get('email') for r in reviewers]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000263
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000264
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000265class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000266 """An instance of OutputApi gets passed to presubmit scripts so that they
267 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000268 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000269 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000270 PresubmitError = _PresubmitError
271 PresubmitPromptWarning = _PresubmitPromptWarning
272 PresubmitNotifyResult = _PresubmitNotifyResult
273 MailTextResult = _MailTextResult
274
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000275 def __init__(self, is_committing):
276 self.is_committing = is_committing
277
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000278 def PresubmitPromptOrNotify(self, *args, **kwargs):
279 """Warn the user when uploading, but only notify if committing."""
280 if self.is_committing:
281 return self.PresubmitNotifyResult(*args, **kwargs)
282 return self.PresubmitPromptWarning(*args, **kwargs)
283
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800284 def EnsureCQIncludeTrybotsAreAdded(self, cl, bots_to_include, message):
285 """Helper for any PostUploadHook wishing to add CQ_INCLUDE_TRYBOTS.
286
287 Merges the bots_to_include into the current CQ_INCLUDE_TRYBOTS list,
288 keeping it alphabetically sorted. Returns the results that should be
289 returned from the PostUploadHook.
290
291 Args:
292 cl: The git_cl.Changelist object.
293 bots_to_include: A list of strings of bots to include, in the form
294 "master:slave".
295 message: A message to be printed in the case that
296 CQ_INCLUDE_TRYBOTS was updated.
297 """
298 description = cl.GetDescription(force=True)
Aaron Gableb584c4f2017-04-26 16:28:08 -0700299 include_re = re.compile(r'^CQ_INCLUDE_TRYBOTS=(.*)$', re.M | re.I)
300
301 prior_bots = []
302 if cl.IsGerrit():
303 trybot_footers = git_footers.parse_footers(description).get(
304 git_footers.normalize_name('Cq-Include-Trybots'), [])
305 for f in trybot_footers:
306 prior_bots += [b.strip() for b in f.split(';') if b.strip()]
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800307 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700308 trybot_tags = include_re.finditer(description)
309 for t in trybot_tags:
310 prior_bots += [b.strip() for b in t.group(1).split(';') if b.strip()]
311
312 if set(prior_bots) >= set(bots_to_include):
313 return []
314 all_bots = ';'.join(sorted(set(prior_bots) | set(bots_to_include)))
315
316 if cl.IsGerrit():
317 description = git_footers.remove_footer(
318 description, 'Cq-Include-Trybots')
319 description = git_footers.add_footer(
320 description, 'Cq-Include-Trybots', all_bots,
321 before_keys=['Change-Id'])
322 else:
323 new_include_trybots = 'CQ_INCLUDE_TRYBOTS=%s' % all_bots
324 m = include_re.search(description)
325 if m:
326 description = include_re.sub(new_include_trybots, description)
Kenneth Russelldf6e7342017-04-24 17:07:41 -0700327 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700328 description = '%s\n%s\n' % (description, new_include_trybots)
329
330 cl.UpdateDescription(description, force=True)
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800331 return [self.PresubmitNotifyResult(message)]
332
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000333
334class InputApi(object):
335 """An instance of this object is passed to presubmit scripts so they can
336 know stuff about the change they're looking at.
337 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000338 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800339 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000340
maruel@chromium.org3410d912009-06-09 20:56:16 +0000341 # File extensions that are considered source files from a style guide
342 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000343 #
344 # Files without an extension aren't included in the list. If you want to
345 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
346 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000347 DEFAULT_WHITE_LIST = (
348 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000349 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
350 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000351 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000352 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000353 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000354 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000355 )
356
357 # Path regexp that should be excluded from being considered containing source
358 # files. Don't modify this list from a presubmit script!
359 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000360 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000361 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000362 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
363 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000364 # Output directories (just in case)
365 r".*\bDebug[\\\/].*",
366 r".*\bRelease[\\\/].*",
367 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000368 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000369 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000370 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000371 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000372 r"(|.*[\\\/])\.git[\\\/].*",
373 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000374 # There is no point in processing a patch file.
375 r".+\.diff$",
376 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000377 )
378
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000379 def __init__(self, change, presubmit_path, is_committing,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000380 rietveld_obj, verbose, gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000381 """Builds an InputApi object.
382
383 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000384 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000385 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000386 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000387 rietveld_obj: rietveld.Rietveld client object
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000388 gerrit_obj: provides basic Gerrit codereview functionality.
389 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000390 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000391 # Version number of the presubmit_support script.
392 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000393 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000394 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000395 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000396 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000397 self.dry_run = dry_run
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000398 # TBD
399 self.host_url = 'http://codereview.chromium.org'
400 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000401 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000402
403 # We expose various modules and functions as attributes of the input_api
404 # so that presubmit scripts don't have to import them.
405 self.basename = os.path.basename
406 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000407 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000408 self.cStringIO = cStringIO
dcheng091b7db2016-06-16 01:27:51 -0700409 self.fnmatch = fnmatch
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000410 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000411 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000412 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000413 self.os_listdir = os.listdir
414 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000415 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000416 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000417 self.pickle = pickle
418 self.marshal = marshal
419 self.re = re
420 self.subprocess = subprocess
421 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000422 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000423 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000424 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000425 self.urllib2 = urllib2
426
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000427 # To easily fork python.
428 self.python_executable = sys.executable
429 self.environ = os.environ
430
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000431 # InputApi.platform is the platform you're currently running on.
432 self.platform = sys.platform
433
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000434 self.cpu_count = multiprocessing.cpu_count()
435
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000436 # this is done here because in RunTests, the current working directory has
437 # changed, which causes Pool() to explode fantastically when run on windows
438 # (because it tries to load the __main__ module, which imports lots of
439 # things relative to the current working directory).
440 self._run_tests_pool = multiprocessing.Pool(self.cpu_count)
441
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000442 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000443 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000444
445 # We carry the canned checks so presubmit scripts can easily use them.
446 self.canned_checks = presubmit_canned_checks
447
Jochen Eisinger72606f82017-04-04 10:44:18 +0200448
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000449 # TODO(dpranke): figure out a list of all approved owners for a repo
450 # in order to be able to handle wildcard OWNERS files?
451 self.owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +0200452 fopen=file, os_path=self.os_path)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200453 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000454 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000455 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000456
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000457 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000458 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000459 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800460 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000461 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000462 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000463 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
464 for (a, b, header) in cpplint._re_pattern_templates
465 ]
466
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000467 def PresubmitLocalPath(self):
468 """Returns the local path of the presubmit script currently being run.
469
470 This is useful if you don't want to hard-code absolute paths in the
471 presubmit script. For example, It can be used to find another file
472 relative to the PRESUBMIT.py script, so the whole tree can be branched and
473 the presubmit script still works, without editing its content.
474 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000475 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000476
agable0b65e732016-11-22 09:25:46 -0800477 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000478 """Same as input_api.change.AffectedFiles() except only lists files
479 (and optionally directories) in the same directory as the current presubmit
480 script, or subdirectories thereof.
481 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000482 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000483 if len(dir_with_slash) == 1:
484 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000485
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000486 return filter(
487 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
agable0b65e732016-11-22 09:25:46 -0800488 self.change.AffectedFiles(include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000489
agable0b65e732016-11-22 09:25:46 -0800490 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000491 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800492 paths = [af.LocalPath() for af in self.AffectedFiles()]
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000493 logging.debug("LocalPaths: %s", paths)
494 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000495
agable0b65e732016-11-22 09:25:46 -0800496 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000497 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800498 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000499
agable0b65e732016-11-22 09:25:46 -0800500 def AffectedTestableFiles(self, include_deletes=None):
501 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000502 in the same directory as the current presubmit script, or subdirectories
503 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000504 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000505 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800506 warn("AffectedTestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000507 " is deprecated and ignored" % str(include_deletes),
508 category=DeprecationWarning,
509 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800510 return filter(lambda x: x.IsTestableFile(),
511 self.AffectedFiles(include_deletes=False))
512
513 def AffectedTextFiles(self, include_deletes=None):
514 """An alias to AffectedTestableFiles for backwards compatibility."""
515 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000516
maruel@chromium.org3410d912009-06-09 20:56:16 +0000517 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
518 """Filters out files that aren't considered "source file".
519
520 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
521 and InputApi.DEFAULT_BLACK_LIST is used respectively.
522
523 The lists will be compiled as regular expression and
524 AffectedFile.LocalPath() needs to pass both list.
525
526 Note: Copy-paste this function to suit your needs or use a lambda function.
527 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000528 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000529 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000530 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000531 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000532 return True
533 return False
534 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
535 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
536
537 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800538 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000539
540 If source_file is None, InputApi.FilterSourceFile() is used.
541 """
542 if not source_file:
543 source_file = self.FilterSourceFile
agable0b65e732016-11-22 09:25:46 -0800544 return filter(source_file, self.AffectedTestableFiles())
maruel@chromium.org3410d912009-06-09 20:56:16 +0000545
546 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000547 """An iterator over all text lines in "new" version of changed files.
548
549 Only lists lines from new or modified text files in the change that are
550 contained by the directory of the currently executing presubmit script.
551
552 This is useful for doing line-by-line regex checks, like checking for
553 trailing whitespace.
554
555 Yields:
556 a 3 tuple:
557 the AffectedFile instance of the current file;
558 integer line number (1-based); and
559 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000560
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000561 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000562 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000563 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000564 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000565
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000566 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000567 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000568
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000569 Deny reading anything outside the repository.
570 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000571 if isinstance(file_item, AffectedFile):
572 file_item = file_item.AbsoluteLocalPath()
573 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000574 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000575 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000576
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000577 @property
578 def tbr(self):
579 """Returns if a change is TBR'ed."""
Jeremy Romandce22502017-06-20 15:37:29 -0400580 return 'TBR' in self.change.tags or self.change.TBRsFromDescription()
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000581
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000582 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000583 tests = []
584 msgs = []
585 for t in tests_mix:
586 if isinstance(t, OutputApi.PresubmitResult):
587 msgs.append(t)
588 else:
589 assert issubclass(t.message, _PresubmitResult)
590 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000591 if self.verbose:
592 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000593 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000594 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000595 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000596 else:
597 msgs.extend(map(CallCommand, tests))
598 return [m for m in msgs if m]
599
scottmg86099d72016-09-01 09:16:51 -0700600 def ShutdownPool(self):
601 self._run_tests_pool.close()
602 self._run_tests_pool.join()
603 self._run_tests_pool = None
604
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000605
nick@chromium.orgff526192013-06-10 19:30:26 +0000606class _DiffCache(object):
607 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000608 def __init__(self, upstream=None):
609 """Stores the upstream revision against which all diffs will be computed."""
610 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000611
612 def GetDiff(self, path, local_root):
613 """Get the diff for a particular path."""
614 raise NotImplementedError()
615
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700616 def GetOldContents(self, path, local_root):
617 """Get the old version for a particular path."""
618 raise NotImplementedError()
619
nick@chromium.orgff526192013-06-10 19:30:26 +0000620
nick@chromium.orgff526192013-06-10 19:30:26 +0000621class _GitDiffCache(_DiffCache):
622 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000623 def __init__(self, upstream):
624 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000625 self._diffs_by_file = None
626
627 def GetDiff(self, path, local_root):
628 if not self._diffs_by_file:
629 # Compute a single diff for all files and parse the output; should
630 # with git this is much faster than computing one diff for each file.
631 diffs = {}
632
633 # Don't specify any filenames below, because there are command line length
634 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000635 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
636 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000637
638 # This regex matches the path twice, separated by a space. Note that
639 # filename itself may contain spaces.
640 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
641 current_diff = []
642 keep_line_endings = True
643 for x in unified_diff.splitlines(keep_line_endings):
644 match = file_marker.match(x)
645 if match:
646 # Marks the start of a new per-file section.
647 diffs[match.group('filename')] = current_diff = [x]
648 elif x.startswith('diff --git'):
649 raise PresubmitFailure('Unexpected diff line: %s' % x)
650 else:
651 current_diff.append(x)
652
653 self._diffs_by_file = dict(
654 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
655
656 if path not in self._diffs_by_file:
657 raise PresubmitFailure(
658 'Unified diff did not contain entry for file %s' % path)
659
660 return self._diffs_by_file[path]
661
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700662 def GetOldContents(self, path, local_root):
663 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
664
nick@chromium.orgff526192013-06-10 19:30:26 +0000665
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000666class AffectedFile(object):
667 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000668
669 DIFF_CACHE = _DiffCache
670
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000671 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800672 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000673 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000674 self._path = path
675 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000676 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000677 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000678 self._cached_changed_contents = None
679 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000680 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700681 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000682
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683 def LocalPath(self):
684 """Returns the path of this file on the local disk relative to client root.
685 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000686 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687
688 def AbsoluteLocalPath(self):
689 """Returns the absolute path of this file on the local disk.
690 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000691 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000692
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000693 def Action(self):
694 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000695 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000696
agable0b65e732016-11-22 09:25:46 -0800697 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000698 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000699
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000700 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000701 raise NotImplementedError() # Implement when needed
702
agable0b65e732016-11-22 09:25:46 -0800703 def IsTextFile(self):
704 """An alias to IsTestableFile for backwards compatibility."""
705 return self.IsTestableFile()
706
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700707 def OldContents(self):
708 """Returns an iterator over the lines in the old version of file.
709
Daniel Cheng2da34fe2017-03-21 20:42:12 -0700710 The old version is the file before any modifications in the user's
711 workspace, i.e. the "left hand side".
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700712
713 Contents will be empty if the file is a directory or does not exist.
714 Note: The carriage returns (LF or CR) are stripped off.
715 """
716 return self._diff_cache.GetOldContents(self.LocalPath(),
717 self._local_root).splitlines()
718
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000719 def NewContents(self):
720 """Returns an iterator over the lines in the new version of file.
721
722 The new version is the file in the user's workspace, i.e. the "right hand
723 side".
724
725 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000726 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000727 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000728 if self._cached_new_contents is None:
729 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800730 try:
731 self._cached_new_contents = gclient_utils.FileRead(
732 self.AbsoluteLocalPath(), 'rU').splitlines()
733 except IOError:
734 pass # File not found? That's fine; maybe it was deleted.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000735 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000736
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000737 def ChangedContents(self):
738 """Returns a list of tuples (line number, line text) of all new lines.
739
740 This relies on the scm diff output describing each changed code section
741 with a line of the form
742
743 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
744 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000745 if self._cached_changed_contents is not None:
746 return self._cached_changed_contents[:]
747 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000748 line_num = 0
749
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000750 for line in self.GenerateScmDiff().splitlines():
751 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
752 if m:
753 line_num = int(m.groups(1)[0])
754 continue
755 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000756 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000757 if not line.startswith('-'):
758 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000759 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000760
maruel@chromium.org5de13972009-06-10 18:16:06 +0000761 def __str__(self):
762 return self.LocalPath()
763
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000764 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000765 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000766
maruel@chromium.org58407af2011-04-12 23:15:57 +0000767
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000768class GitAffectedFile(AffectedFile):
769 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000770 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800771 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000772
nick@chromium.orgff526192013-06-10 19:30:26 +0000773 DIFF_CACHE = _GitDiffCache
774
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000775 def __init__(self, *args, **kwargs):
776 AffectedFile.__init__(self, *args, **kwargs)
777 self._server_path = None
agable0b65e732016-11-22 09:25:46 -0800778 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000779
agable0b65e732016-11-22 09:25:46 -0800780 def IsTestableFile(self):
781 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000782 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -0800783 # A deleted file is not testable.
784 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000785 else:
agable0b65e732016-11-22 09:25:46 -0800786 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
787 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000788
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000789
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000790class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000791 """Describe a change.
792
793 Used directly by the presubmit scripts to query the current change being
794 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000795
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000796 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000797 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000798 self.KEY: equivalent to tags['KEY']
799 """
800
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000801 _AFFECTED_FILES = AffectedFile
802
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000803 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000804 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000805 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000806 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000807
maruel@chromium.org58407af2011-04-12 23:15:57 +0000808 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000809 self, name, description, local_root, files, issue, patchset, author,
810 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000811 if files is None:
812 files = []
813 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000814 # Convert root into an absolute path.
815 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000816 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000817 self.issue = issue
818 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000819 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000820
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000821 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000822 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000823 self._description_without_tags = ''
824 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000825
maruel@chromium.orge085d812011-10-10 19:49:15 +0000826 assert all(
827 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
828
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000829 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000830 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000831 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
832 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000833 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000834
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000835 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000836 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000837 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000838
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000839 def DescriptionText(self):
840 """Returns the user-entered changelist description, minus tags.
841
842 Any line in the user-provided description starting with e.g. "FOO="
843 (whitespace permitted before and around) is considered a tag line. Such
844 lines are stripped out of the description this function returns.
845 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000846 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000847
848 def FullDescriptionText(self):
849 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000850 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000851
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000852 def SetDescriptionText(self, description):
853 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000854
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000855 Also updates the list of tags."""
856 self._full_description = description
857
858 # From the description text, build up a dictionary of key/value pairs
859 # plus the description minus all key/value or "tag" lines.
860 description_without_tags = []
861 self.tags = {}
862 for line in self._full_description.splitlines():
863 m = self.TAG_LINE_RE.match(line)
864 if m:
865 self.tags[m.group('key')] = m.group('value')
866 else:
867 description_without_tags.append(line)
868
869 # Change back to text and remove whitespace at end.
870 self._description_without_tags = (
871 '\n'.join(description_without_tags).rstrip())
872
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000873 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000874 """Returns the repository (checkout) root directory for this change,
875 as an absolute path.
876 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000877 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000878
879 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000880 """Return tags directly as attributes on the object."""
881 if not re.match(r"^[A-Z_]*$", attr):
882 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000883 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000884
Aaron Gablefc03e672017-05-15 14:09:42 -0700885 def BugsFromDescription(self):
886 """Returns all bugs referenced in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700887 tags = [b.strip() for b in self.tags.get('BUG', '').split(',') if b.strip()]
888 footers = git_footers.parse_footers(self._full_description).get('Bug', [])
889 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -0700890
891 def ReviewersFromDescription(self):
892 """Returns all reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700893 # We don't support a "R:" git-footer for reviewers; that is in metadata.
894 tags = [r.strip() for r in self.tags.get('R', '').split(',') if r.strip()]
895 return sorted(set(tags))
Aaron Gablefc03e672017-05-15 14:09:42 -0700896
897 def TBRsFromDescription(self):
898 """Returns all TBR reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700899 tags = [r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()]
900 # TODO(agable): Remove support for 'Tbr:' when TBRs are programmatically
901 # determined by self-CR+1s.
902 footers = git_footers.parse_footers(self._full_description).get('Tbr', [])
903 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -0700904
905 # TODO(agable): Delete these once we're sure they're unused.
906 @property
907 def BUG(self):
908 return ','.join(self.BugsFromDescription())
909 @property
910 def R(self):
911 return ','.join(self.ReviewersFromDescription())
912 @property
913 def TBR(self):
914 return ','.join(self.TBRsFromDescription())
915
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000916 def AllFiles(self, root=None):
917 """List all files under source control in the repo."""
918 raise NotImplementedError()
919
agable0b65e732016-11-22 09:25:46 -0800920 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000921 """Returns a list of AffectedFile instances for all files in the change.
922
923 Args:
924 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +0000925 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000926
927 Returns:
928 [AffectedFile(path, action), AffectedFile(path, action)]
929 """
agable0b65e732016-11-22 09:25:46 -0800930 affected = filter(file_filter, self._affected_files)
sail@chromium.org5538e022011-05-12 17:53:16 +0000931
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000932 if include_deletes:
933 return affected
Lei Zhang9611c4c2017-04-04 01:41:56 -0700934 return filter(lambda x: x.Action() != 'D', affected)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000935
agable0b65e732016-11-22 09:25:46 -0800936 def AffectedTestableFiles(self, include_deletes=None):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000937 """Return a list of the existing text files in a change."""
938 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800939 warn("AffectedTeestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000940 " is deprecated and ignored" % str(include_deletes),
941 category=DeprecationWarning,
942 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800943 return filter(lambda x: x.IsTestableFile(),
944 self.AffectedFiles(include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000945
agable0b65e732016-11-22 09:25:46 -0800946 def AffectedTextFiles(self, include_deletes=None):
947 """An alias to AffectedTestableFiles for backwards compatibility."""
948 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000949
agable0b65e732016-11-22 09:25:46 -0800950 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000951 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800952 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000953
agable0b65e732016-11-22 09:25:46 -0800954 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000955 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800956 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000957
958 def RightHandSideLines(self):
959 """An iterator over all text lines in "new" version of changed files.
960
961 Lists lines from new or modified text files in the change.
962
963 This is useful for doing line-by-line regex checks, like checking for
964 trailing whitespace.
965
966 Yields:
967 a 3 tuple:
968 the AffectedFile instance of the current file;
969 integer line number (1-based); and
970 the contents of the line as a string.
971 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000972 return _RightHandSideLinesImpl(
973 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -0800974 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000975
Jochen Eisingerd0573ec2017-04-13 10:55:06 +0200976 def OriginalOwnersFiles(self):
977 """A map from path names of affected OWNERS files to their old content."""
978 def owners_file_filter(f):
979 return 'OWNERS' in os.path.split(f.LocalPath())[1]
980 files = self.AffectedFiles(file_filter=owners_file_filter)
981 return dict([(f.LocalPath(), f.OldContents()) for f in files])
982
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000983
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000984class GitChange(Change):
985 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000986 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000987
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000988 def AllFiles(self, root=None):
989 """List all files under source control in the repo."""
990 root = root or self.RepositoryRoot()
991 return subprocess.check_output(
992 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
993
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000994
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000995def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000996 """Finds all presubmit files that apply to a given set of source files.
997
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000998 If inherit-review-settings-ok is present right under root, looks for
999 PRESUBMIT.py in directories enclosing root.
1000
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001001 Args:
1002 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001003 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001004
1005 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001006 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001007 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001008 files = [normpath(os.path.join(root, f)) for f in files]
1009
1010 # List all the individual directories containing files.
1011 directories = set([os.path.dirname(f) for f in files])
1012
1013 # Ignore root if inherit-review-settings-ok is present.
1014 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1015 root = None
1016
1017 # Collect all unique directories that may contain PRESUBMIT.py.
1018 candidates = set()
1019 for directory in directories:
1020 while True:
1021 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001022 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001023 candidates.add(directory)
1024 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001025 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001026 parent_dir = os.path.dirname(directory)
1027 if parent_dir == directory:
1028 # We hit the system root directory.
1029 break
1030 directory = parent_dir
1031
1032 # Look for PRESUBMIT.py in all candidate directories.
1033 results = []
1034 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -07001035 try:
1036 for f in os.listdir(directory):
1037 p = os.path.join(directory, f)
1038 if os.path.isfile(p) and re.match(
1039 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1040 results.append(p)
1041 except OSError:
1042 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001043
tobiasjs2836bcf2016-08-16 04:08:16 -07001044 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001045 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001046
1047
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001048class GetTryMastersExecuter(object):
1049 @staticmethod
1050 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1051 """Executes GetPreferredTryMasters() from a single presubmit script.
1052
1053 Args:
1054 script_text: The text of the presubmit script.
1055 presubmit_path: Project script to run.
1056 project: Project name to pass to presubmit script for bot selection.
1057
1058 Return:
1059 A map of try masters to map of builders to set of tests.
1060 """
1061 context = {}
1062 try:
1063 exec script_text in context
1064 except Exception, e:
1065 raise PresubmitFailure('"%s" had an exception.\n%s'
1066 % (presubmit_path, e))
1067
1068 function_name = 'GetPreferredTryMasters'
1069 if function_name not in context:
1070 return {}
1071 get_preferred_try_masters = context[function_name]
1072 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1073 raise PresubmitFailure(
1074 'Expected function "GetPreferredTryMasters" to take two arguments.')
1075 return get_preferred_try_masters(project, change)
1076
1077
rmistry@google.com5626a922015-02-26 14:03:30 +00001078class GetPostUploadExecuter(object):
1079 @staticmethod
1080 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1081 """Executes PostUploadHook() from a single presubmit script.
1082
1083 Args:
1084 script_text: The text of the presubmit script.
1085 presubmit_path: Project script to run.
1086 cl: The Changelist object.
1087 change: The Change object.
1088
1089 Return:
1090 A list of results objects.
1091 """
1092 context = {}
1093 try:
1094 exec script_text in context
1095 except Exception, e:
1096 raise PresubmitFailure('"%s" had an exception.\n%s'
1097 % (presubmit_path, e))
1098
1099 function_name = 'PostUploadHook'
1100 if function_name not in context:
1101 return {}
1102 post_upload_hook = context[function_name]
1103 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1104 raise PresubmitFailure(
1105 'Expected function "PostUploadHook" to take three arguments.')
1106 return post_upload_hook(cl, change, OutputApi(False))
1107
1108
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001109def _MergeMasters(masters1, masters2):
1110 """Merges two master maps. Merges also the tests of each builder."""
1111 result = {}
1112 for (master, builders) in itertools.chain(masters1.iteritems(),
1113 masters2.iteritems()):
1114 new_builders = result.setdefault(master, {})
1115 for (builder, tests) in builders.iteritems():
1116 new_builders.setdefault(builder, set([])).update(tests)
1117 return result
1118
1119
1120def DoGetTryMasters(change,
1121 changed_files,
1122 repository_root,
1123 default_presubmit,
1124 project,
1125 verbose,
1126 output_stream):
1127 """Get the list of try masters from the presubmit scripts.
1128
1129 Args:
1130 changed_files: List of modified files.
1131 repository_root: The repository root.
1132 default_presubmit: A default presubmit script to execute in any case.
1133 project: Optional name of a project used in selecting trybots.
1134 verbose: Prints debug info.
1135 output_stream: A stream to write debug output to.
1136
1137 Return:
1138 Map of try masters to map of builders to set of tests.
1139 """
1140 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1141 if not presubmit_files and verbose:
1142 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1143 results = {}
1144 executer = GetTryMastersExecuter()
1145
1146 if default_presubmit:
1147 if verbose:
1148 output_stream.write("Running default presubmit script.\n")
1149 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1150 results = _MergeMasters(results, executer.ExecPresubmitScript(
1151 default_presubmit, fake_path, project, change))
1152 for filename in presubmit_files:
1153 filename = os.path.abspath(filename)
1154 if verbose:
1155 output_stream.write("Running %s\n" % filename)
1156 # Accept CRLF presubmit script.
1157 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1158 results = _MergeMasters(results, executer.ExecPresubmitScript(
1159 presubmit_script, filename, project, change))
1160
1161 # Make sets to lists again for later JSON serialization.
1162 for builders in results.itervalues():
1163 for builder in builders:
1164 builders[builder] = list(builders[builder])
1165
1166 if results and verbose:
1167 output_stream.write('%s\n' % str(results))
1168 return results
1169
1170
rmistry@google.com5626a922015-02-26 14:03:30 +00001171def DoPostUploadExecuter(change,
1172 cl,
1173 repository_root,
1174 verbose,
1175 output_stream):
1176 """Execute the post upload hook.
1177
1178 Args:
1179 change: The Change object.
1180 cl: The Changelist object.
1181 repository_root: The repository root.
1182 verbose: Prints debug info.
1183 output_stream: A stream to write debug output to.
1184 """
1185 presubmit_files = ListRelevantPresubmitFiles(
1186 change.LocalPaths(), repository_root)
1187 if not presubmit_files and verbose:
1188 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1189 results = []
1190 executer = GetPostUploadExecuter()
1191 # The root presubmit file should be executed after the ones in subdirectories.
1192 # i.e. the specific post upload hooks should run before the general ones.
1193 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1194 presubmit_files.reverse()
1195
1196 for filename in presubmit_files:
1197 filename = os.path.abspath(filename)
1198 if verbose:
1199 output_stream.write("Running %s\n" % filename)
1200 # Accept CRLF presubmit script.
1201 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1202 results.extend(executer.ExecPresubmitScript(
1203 presubmit_script, filename, cl, change))
1204 output_stream.write('\n')
1205 if results:
1206 output_stream.write('** Post Upload Hook Messages **\n')
1207 for result in results:
1208 result.handle(output_stream)
1209 output_stream.write('\n')
1210
1211 return results
1212
1213
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001214class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001215 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001216 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001217 """
1218 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001219 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001220 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001221 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001222 gerrit_obj: provides basic Gerrit codereview functionality.
1223 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001224 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001225 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001226 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001227 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001228 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001229 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001230 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001231
1232 def ExecPresubmitScript(self, script_text, presubmit_path):
1233 """Executes a single presubmit script.
1234
1235 Args:
1236 script_text: The text of the presubmit script.
1237 presubmit_path: The path to the presubmit file (this will be reported via
1238 input_api.PresubmitLocalPath()).
1239
1240 Return:
1241 A list of result objects, empty if no problems.
1242 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001243
chase@chromium.org8e416c82009-10-06 04:30:44 +00001244 # Change to the presubmit file's directory to support local imports.
1245 main_path = os.getcwd()
1246 os.chdir(os.path.dirname(presubmit_path))
1247
1248 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001249 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001250 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001251 gerrit_obj=self.gerrit, dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001252 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001253 try:
1254 exec script_text in context
1255 except Exception, e:
1256 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001257
1258 # These function names must change if we make substantial changes to
1259 # the presubmit API that are not backwards compatible.
1260 if self.committing:
1261 function_name = 'CheckChangeOnCommit'
1262 else:
1263 function_name = 'CheckChangeOnUpload'
1264 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001265 context['__args'] = (input_api, OutputApi(self.committing))
tobiasjs2836bcf2016-08-16 04:08:16 -07001266 logging.debug('Running %s in %s', function_name, presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001267 result = eval(function_name + '(*__args)', context)
tobiasjs2836bcf2016-08-16 04:08:16 -07001268 logging.debug('Running %s done.', function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001269 if not (isinstance(result, types.TupleType) or
1270 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001271 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001272 'Presubmit functions must return a tuple or list')
1273 for item in result:
1274 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001275 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001276 'All presubmit results must be of types derived from '
1277 'output_api.PresubmitResult')
1278 else:
1279 result = () # no error since the script doesn't care about current event.
1280
scottmg86099d72016-09-01 09:16:51 -07001281 input_api.ShutdownPool()
1282
chase@chromium.org8e416c82009-10-06 04:30:44 +00001283 # Return the process to the original working directory.
1284 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001285 return result
1286
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001287def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001288 committing,
1289 verbose,
1290 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001291 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001292 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001293 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001294 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001295 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001296 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001297 """Runs all presubmit checks that apply to the files in the change.
1298
1299 This finds all PRESUBMIT.py files in directories enclosing the files in the
1300 change (up to the repository root) and calls the relevant entrypoint function
1301 depending on whether the change is being committed or uploaded.
1302
1303 Prints errors, warnings and notifications. Prompts the user for warnings
1304 when needed.
1305
1306 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001307 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001308 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001309 verbose: Prints debug info.
1310 output_stream: A stream to write output from presubmit tests to.
1311 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001312 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001313 may_prompt: Enable (y/n) questions on warning or error. If False,
1314 any questions are answered with yes by default.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001315 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001316 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001317 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001318
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001319 Warning:
1320 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1321 SHOULD be sys.stdin.
1322
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001323 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001324 A PresubmitOutput object. Use output.should_continue() to figure out
1325 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001326 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001327 old_environ = os.environ
1328 try:
1329 # Make sure python subprocesses won't generate .pyc files.
1330 os.environ = os.environ.copy()
1331 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001332
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001333 output = PresubmitOutput(input_stream, output_stream)
1334 if committing:
1335 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001336 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001337 output.write("Running presubmit upload checks ...\n")
1338 start_time = time.time()
1339 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001340 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001341 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001342 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001343 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001344 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001345 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001346 if default_presubmit:
1347 if verbose:
1348 output.write("Running default presubmit script.\n")
1349 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1350 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1351 for filename in presubmit_files:
1352 filename = os.path.abspath(filename)
1353 if verbose:
1354 output.write("Running %s\n" % filename)
1355 # Accept CRLF presubmit script.
1356 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1357 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001358
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001359 errors = []
1360 notifications = []
1361 warnings = []
1362 for result in results:
1363 if result.fatal:
1364 errors.append(result)
1365 elif result.should_prompt:
1366 warnings.append(result)
1367 else:
1368 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001369
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001370 output.write('\n')
1371 for name, items in (('Messages', notifications),
1372 ('Warnings', warnings),
1373 ('ERRORS', errors)):
1374 if items:
1375 output.write('** Presubmit %s **\n' % name)
1376 for item in items:
1377 item.handle(output)
1378 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001379
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001380 total_time = time.time() - start_time
1381 if total_time > 1.0:
1382 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001383
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001384 if errors:
1385 output.fail()
1386 elif warnings:
1387 output.write('There were presubmit warnings. ')
1388 if may_prompt:
1389 output.prompt_yes_no('Are you sure you wish to continue? (y/N): ')
1390 else:
1391 output.write('Presubmit checks passed.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001392
1393 global _ASKED_FOR_FEEDBACK
1394 # Ask for feedback one time out of 5.
1395 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001396 output.write(
1397 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1398 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1399 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001400 _ASKED_FOR_FEEDBACK = True
1401 return output
1402 finally:
1403 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001404
1405
1406def ScanSubDirs(mask, recursive):
1407 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001408 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001409
1410 results = []
1411 for root, dirs, files in os.walk('.'):
1412 if '.svn' in dirs:
1413 dirs.remove('.svn')
1414 if '.git' in dirs:
1415 dirs.remove('.git')
1416 for name in files:
1417 if fnmatch.fnmatch(name, mask):
1418 results.append(os.path.join(root, name))
1419 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001420
1421
1422def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001423 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001424 files = []
1425 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001426 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001427 return files
1428
1429
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001430def load_files(options, args):
1431 """Tries to determine the SCM."""
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001432 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001433 if args:
1434 files = ParseFiles(args, options.recursive)
agable0b65e732016-11-22 09:25:46 -08001435 change_scm = scm.determine_scm(options.root)
1436 if change_scm == 'git':
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001437 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001438 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001439 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001440 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001441 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001442 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001443 if not files:
1444 return None, None
1445 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001446 return change_class, files
1447
1448
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001449class NonexistantCannedCheckFilter(Exception):
1450 pass
1451
1452
1453@contextlib.contextmanager
1454def canned_check_filter(method_names):
1455 filtered = {}
1456 try:
1457 for method_name in method_names:
1458 if not hasattr(presubmit_canned_checks, method_name):
1459 raise NonexistantCannedCheckFilter(method_name)
1460 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1461 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1462 yield
1463 finally:
1464 for name, method in filtered.iteritems():
1465 setattr(presubmit_canned_checks, name, method)
1466
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001467
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001468def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001469 """Runs an external program, potentially from a child process created by the
1470 multiprocessing module.
1471
1472 multiprocessing needs a top level function with a single argument.
1473 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001474 cmd_data.kwargs['stdout'] = subprocess.PIPE
1475 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1476 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001477 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001478 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001479 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001480 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001481 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001482 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001483 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1484 if code != 0:
1485 return cmd_data.message(
1486 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1487 if cmd_data.info:
1488 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001489
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001490
sbc@chromium.org013731e2015-02-26 18:28:43 +00001491def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001492 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001493 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001494 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001495 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001496 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1497 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001498 parser.add_option("-r", "--recursive", action="store_true",
1499 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001500 parser.add_option("-v", "--verbose", action="count", default=0,
1501 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001502 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001503 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001504 parser.add_option("--description", default='')
1505 parser.add_option("--issue", type='int', default=0)
1506 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001507 parser.add_option("--root", default=os.getcwd(),
1508 help="Search for PRESUBMIT.py up to this directory. "
1509 "If inherit-review-settings-ok is present in this "
1510 "directory, parent directories up to the root file "
1511 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001512 parser.add_option("--upstream",
1513 help="Git only: the base ref or upstream branch against "
1514 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001515 parser.add_option("--default_presubmit")
1516 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001517 parser.add_option("--skip_canned", action='append', default=[],
1518 help="A list of checks to skip which appear in "
1519 "presubmit_canned_checks. Can be provided multiple times "
1520 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001521 parser.add_option("--dry_run", action='store_true',
1522 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001523 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001524 parser.add_option("--gerrit_fetch", action='store_true',
1525 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001526 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1527 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001528 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1529 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001530 # These are for OAuth2 authentication for bots. See also apply_issue.py
1531 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1532 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1533
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001534 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001535 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001536 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001537
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001538 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001539 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001540 elif options.verbose:
1541 logging.basicConfig(level=logging.INFO)
1542 else:
1543 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001544
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001545 if (any((options.rietveld_url, options.rietveld_email_file,
1546 options.rietveld_fetch, options.rietveld_private_key_file))
1547 and any((options.gerrit_url, options.gerrit_fetch))):
1548 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1549 'allowed')
1550
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001551 if options.rietveld_email and options.rietveld_email_file:
1552 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1553 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001554 if options.rietveld_email_file:
1555 with open(options.rietveld_email_file, "rb") as f:
1556 options.rietveld_email = f.read().strip()
1557
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001558 change_class, files = load_files(options, args)
1559 if not change_class:
1560 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001561 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001562
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001563 rietveld_obj, gerrit_obj = None, None
1564
maruel@chromium.org239f4112011-06-03 20:08:23 +00001565 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001566 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001567 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001568 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1569 options.rietveld_url,
1570 options.rietveld_email,
1571 options.rietveld_private_key_file)
1572 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001573 rietveld_obj = rietveld.CachingRietveld(
1574 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001575 auth_config,
1576 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001577 if options.rietveld_fetch:
1578 assert options.issue
1579 props = rietveld_obj.get_issue_properties(options.issue, False)
1580 options.author = props['owner_email']
1581 options.description = props['description']
1582 logging.info('Got author: "%s"', options.author)
1583 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001584
1585 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001586 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001587 rietveld_obj = None
1588 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1589 options.author = gerrit_obj.GetChangeOwner(options.issue)
1590 options.description = gerrit_obj.GetChangeDescription(options.issue,
1591 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001592 logging.info('Got author: "%s"', options.author)
1593 logging.info('Got description: """\n%s\n"""', options.description)
1594
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001595 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001596 with canned_check_filter(options.skip_canned):
1597 results = DoPresubmitChecks(
1598 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001599 options.description,
1600 options.root,
1601 files,
1602 options.issue,
1603 options.patchset,
1604 options.author,
1605 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001606 options.commit,
1607 options.verbose,
1608 sys.stdout,
1609 sys.stdin,
1610 options.default_presubmit,
1611 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001612 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001613 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001614 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001615 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001616 except NonexistantCannedCheckFilter, e:
1617 print >> sys.stderr, (
1618 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1619 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001620 except PresubmitFailure, e:
1621 print >> sys.stderr, e
1622 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1623 print >> sys.stderr, 'If all fails, contact maruel@'
1624 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001625
1626
1627if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001628 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001629 try:
1630 sys.exit(main())
1631 except KeyboardInterrupt:
1632 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001633 sys.exit(2)