blob: 89518b8b2b29966ea6500c67a2d90a6e00868643 [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):
agable565adb52016-07-22 14:48:07 -0700250 cr = self.GetChangeInfo(issue)['labels']['Code-Review']
251 max_value = max(int(k) for k in cr['values'].keys())
Aaron Gablef5644a92016-12-02 15:31:58 -0800252 return [r.get('email') for r in cr.get('all', [])
agable565adb52016-07-22 14:48:07 -0700253 if not approving_only or r.get('value', 0) == max_value]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000254
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000255
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000257 """An instance of OutputApi gets passed to presubmit scripts so that they
258 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000259 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000260 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000261 PresubmitError = _PresubmitError
262 PresubmitPromptWarning = _PresubmitPromptWarning
263 PresubmitNotifyResult = _PresubmitNotifyResult
264 MailTextResult = _MailTextResult
265
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000266 def __init__(self, is_committing):
267 self.is_committing = is_committing
268
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000269 def PresubmitPromptOrNotify(self, *args, **kwargs):
270 """Warn the user when uploading, but only notify if committing."""
271 if self.is_committing:
272 return self.PresubmitNotifyResult(*args, **kwargs)
273 return self.PresubmitPromptWarning(*args, **kwargs)
274
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800275 def EnsureCQIncludeTrybotsAreAdded(self, cl, bots_to_include, message):
276 """Helper for any PostUploadHook wishing to add CQ_INCLUDE_TRYBOTS.
277
278 Merges the bots_to_include into the current CQ_INCLUDE_TRYBOTS list,
279 keeping it alphabetically sorted. Returns the results that should be
280 returned from the PostUploadHook.
281
282 Args:
283 cl: The git_cl.Changelist object.
284 bots_to_include: A list of strings of bots to include, in the form
285 "master:slave".
286 message: A message to be printed in the case that
287 CQ_INCLUDE_TRYBOTS was updated.
288 """
289 description = cl.GetDescription(force=True)
Aaron Gableb584c4f2017-04-26 16:28:08 -0700290 include_re = re.compile(r'^CQ_INCLUDE_TRYBOTS=(.*)$', re.M | re.I)
291
292 prior_bots = []
293 if cl.IsGerrit():
294 trybot_footers = git_footers.parse_footers(description).get(
295 git_footers.normalize_name('Cq-Include-Trybots'), [])
296 for f in trybot_footers:
297 prior_bots += [b.strip() for b in f.split(';') if b.strip()]
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800298 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700299 trybot_tags = include_re.finditer(description)
300 for t in trybot_tags:
301 prior_bots += [b.strip() for b in t.group(1).split(';') if b.strip()]
302
303 if set(prior_bots) >= set(bots_to_include):
304 return []
305 all_bots = ';'.join(sorted(set(prior_bots) | set(bots_to_include)))
306
307 if cl.IsGerrit():
308 description = git_footers.remove_footer(
309 description, 'Cq-Include-Trybots')
310 description = git_footers.add_footer(
311 description, 'Cq-Include-Trybots', all_bots,
312 before_keys=['Change-Id'])
313 else:
314 new_include_trybots = 'CQ_INCLUDE_TRYBOTS=%s' % all_bots
315 m = include_re.search(description)
316 if m:
317 description = include_re.sub(new_include_trybots, description)
Kenneth Russelldf6e7342017-04-24 17:07:41 -0700318 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700319 description = '%s\n%s\n' % (description, new_include_trybots)
320
321 cl.UpdateDescription(description, force=True)
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800322 return [self.PresubmitNotifyResult(message)]
323
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000324
325class InputApi(object):
326 """An instance of this object is passed to presubmit scripts so they can
327 know stuff about the change they're looking at.
328 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000329 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800330 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000331
maruel@chromium.org3410d912009-06-09 20:56:16 +0000332 # File extensions that are considered source files from a style guide
333 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000334 #
335 # Files without an extension aren't included in the list. If you want to
336 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
337 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000338 DEFAULT_WHITE_LIST = (
339 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000340 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
341 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000342 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000343 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000344 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000345 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000346 )
347
348 # Path regexp that should be excluded from being considered containing source
349 # files. Don't modify this list from a presubmit script!
350 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000351 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000352 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000353 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
354 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000355 # Output directories (just in case)
356 r".*\bDebug[\\\/].*",
357 r".*\bRelease[\\\/].*",
358 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000359 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000360 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000361 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000362 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000363 r"(|.*[\\\/])\.git[\\\/].*",
364 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000365 # There is no point in processing a patch file.
366 r".+\.diff$",
367 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000368 )
369
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000370 def __init__(self, change, presubmit_path, is_committing,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000371 rietveld_obj, verbose, gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000372 """Builds an InputApi object.
373
374 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000375 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000376 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000377 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000378 rietveld_obj: rietveld.Rietveld client object
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000379 gerrit_obj: provides basic Gerrit codereview functionality.
380 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000381 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000382 # Version number of the presubmit_support script.
383 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000384 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000385 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000386 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000387 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000388 self.dry_run = dry_run
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000389 # TBD
390 self.host_url = 'http://codereview.chromium.org'
391 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000392 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000393
394 # We expose various modules and functions as attributes of the input_api
395 # so that presubmit scripts don't have to import them.
396 self.basename = os.path.basename
397 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000398 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000399 self.cStringIO = cStringIO
dcheng091b7db2016-06-16 01:27:51 -0700400 self.fnmatch = fnmatch
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000401 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000402 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000403 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000404 self.os_listdir = os.listdir
405 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000406 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000407 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000408 self.pickle = pickle
409 self.marshal = marshal
410 self.re = re
411 self.subprocess = subprocess
412 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000413 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000414 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000415 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000416 self.urllib2 = urllib2
417
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000418 # To easily fork python.
419 self.python_executable = sys.executable
420 self.environ = os.environ
421
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000422 # InputApi.platform is the platform you're currently running on.
423 self.platform = sys.platform
424
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000425 self.cpu_count = multiprocessing.cpu_count()
426
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000427 # this is done here because in RunTests, the current working directory has
428 # changed, which causes Pool() to explode fantastically when run on windows
429 # (because it tries to load the __main__ module, which imports lots of
430 # things relative to the current working directory).
431 self._run_tests_pool = multiprocessing.Pool(self.cpu_count)
432
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000433 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000434 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000435
436 # We carry the canned checks so presubmit scripts can easily use them.
437 self.canned_checks = presubmit_canned_checks
438
Jochen Eisinger72606f82017-04-04 10:44:18 +0200439
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000440 # TODO(dpranke): figure out a list of all approved owners for a repo
441 # in order to be able to handle wildcard OWNERS files?
442 self.owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +0200443 fopen=file, os_path=self.os_path)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200444 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000445 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000446 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000447
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000448 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000449 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000450 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800451 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000452 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000453 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000454 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
455 for (a, b, header) in cpplint._re_pattern_templates
456 ]
457
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000458 def PresubmitLocalPath(self):
459 """Returns the local path of the presubmit script currently being run.
460
461 This is useful if you don't want to hard-code absolute paths in the
462 presubmit script. For example, It can be used to find another file
463 relative to the PRESUBMIT.py script, so the whole tree can be branched and
464 the presubmit script still works, without editing its content.
465 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000466 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000467
agable0b65e732016-11-22 09:25:46 -0800468 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000469 """Same as input_api.change.AffectedFiles() except only lists files
470 (and optionally directories) in the same directory as the current presubmit
471 script, or subdirectories thereof.
472 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000473 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000474 if len(dir_with_slash) == 1:
475 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000476
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000477 return filter(
478 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
agable0b65e732016-11-22 09:25:46 -0800479 self.change.AffectedFiles(include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000480
agable0b65e732016-11-22 09:25:46 -0800481 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000482 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800483 paths = [af.LocalPath() for af in self.AffectedFiles()]
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000484 logging.debug("LocalPaths: %s", paths)
485 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000486
agable0b65e732016-11-22 09:25:46 -0800487 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000488 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800489 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000490
agable0b65e732016-11-22 09:25:46 -0800491 def AffectedTestableFiles(self, include_deletes=None):
492 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000493 in the same directory as the current presubmit script, or subdirectories
494 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000495 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000496 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800497 warn("AffectedTestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000498 " is deprecated and ignored" % str(include_deletes),
499 category=DeprecationWarning,
500 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800501 return filter(lambda x: x.IsTestableFile(),
502 self.AffectedFiles(include_deletes=False))
503
504 def AffectedTextFiles(self, include_deletes=None):
505 """An alias to AffectedTestableFiles for backwards compatibility."""
506 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000507
maruel@chromium.org3410d912009-06-09 20:56:16 +0000508 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
509 """Filters out files that aren't considered "source file".
510
511 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
512 and InputApi.DEFAULT_BLACK_LIST is used respectively.
513
514 The lists will be compiled as regular expression and
515 AffectedFile.LocalPath() needs to pass both list.
516
517 Note: Copy-paste this function to suit your needs or use a lambda function.
518 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000519 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000520 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000521 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000522 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000523 return True
524 return False
525 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
526 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
527
528 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800529 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000530
531 If source_file is None, InputApi.FilterSourceFile() is used.
532 """
533 if not source_file:
534 source_file = self.FilterSourceFile
agable0b65e732016-11-22 09:25:46 -0800535 return filter(source_file, self.AffectedTestableFiles())
maruel@chromium.org3410d912009-06-09 20:56:16 +0000536
537 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000538 """An iterator over all text lines in "new" version of changed files.
539
540 Only lists lines from new or modified text files in the change that are
541 contained by the directory of the currently executing presubmit script.
542
543 This is useful for doing line-by-line regex checks, like checking for
544 trailing whitespace.
545
546 Yields:
547 a 3 tuple:
548 the AffectedFile instance of the current file;
549 integer line number (1-based); and
550 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000551
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000552 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000553 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000554 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000555 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000556
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000557 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000558 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000559
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000560 Deny reading anything outside the repository.
561 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000562 if isinstance(file_item, AffectedFile):
563 file_item = file_item.AbsoluteLocalPath()
564 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000565 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000566 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000567
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000568 @property
569 def tbr(self):
570 """Returns if a change is TBR'ed."""
571 return 'TBR' in self.change.tags
572
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000573 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000574 tests = []
575 msgs = []
576 for t in tests_mix:
577 if isinstance(t, OutputApi.PresubmitResult):
578 msgs.append(t)
579 else:
580 assert issubclass(t.message, _PresubmitResult)
581 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000582 if self.verbose:
583 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000584 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000585 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000586 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000587 else:
588 msgs.extend(map(CallCommand, tests))
589 return [m for m in msgs if m]
590
scottmg86099d72016-09-01 09:16:51 -0700591 def ShutdownPool(self):
592 self._run_tests_pool.close()
593 self._run_tests_pool.join()
594 self._run_tests_pool = None
595
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000596
nick@chromium.orgff526192013-06-10 19:30:26 +0000597class _DiffCache(object):
598 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000599 def __init__(self, upstream=None):
600 """Stores the upstream revision against which all diffs will be computed."""
601 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000602
603 def GetDiff(self, path, local_root):
604 """Get the diff for a particular path."""
605 raise NotImplementedError()
606
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700607 def GetOldContents(self, path, local_root):
608 """Get the old version for a particular path."""
609 raise NotImplementedError()
610
nick@chromium.orgff526192013-06-10 19:30:26 +0000611
nick@chromium.orgff526192013-06-10 19:30:26 +0000612class _GitDiffCache(_DiffCache):
613 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000614 def __init__(self, upstream):
615 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000616 self._diffs_by_file = None
617
618 def GetDiff(self, path, local_root):
619 if not self._diffs_by_file:
620 # Compute a single diff for all files and parse the output; should
621 # with git this is much faster than computing one diff for each file.
622 diffs = {}
623
624 # Don't specify any filenames below, because there are command line length
625 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000626 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
627 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000628
629 # This regex matches the path twice, separated by a space. Note that
630 # filename itself may contain spaces.
631 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
632 current_diff = []
633 keep_line_endings = True
634 for x in unified_diff.splitlines(keep_line_endings):
635 match = file_marker.match(x)
636 if match:
637 # Marks the start of a new per-file section.
638 diffs[match.group('filename')] = current_diff = [x]
639 elif x.startswith('diff --git'):
640 raise PresubmitFailure('Unexpected diff line: %s' % x)
641 else:
642 current_diff.append(x)
643
644 self._diffs_by_file = dict(
645 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
646
647 if path not in self._diffs_by_file:
648 raise PresubmitFailure(
649 'Unified diff did not contain entry for file %s' % path)
650
651 return self._diffs_by_file[path]
652
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700653 def GetOldContents(self, path, local_root):
654 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
655
nick@chromium.orgff526192013-06-10 19:30:26 +0000656
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000657class AffectedFile(object):
658 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000659
660 DIFF_CACHE = _DiffCache
661
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000662 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800663 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000664 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000665 self._path = path
666 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000667 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000668 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000669 self._cached_changed_contents = None
670 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000671 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700672 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000673
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000674 def LocalPath(self):
675 """Returns the path of this file on the local disk relative to client root.
676 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000677 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000678
679 def AbsoluteLocalPath(self):
680 """Returns the absolute path of this file on the local disk.
681 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000682 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684 def Action(self):
685 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000686 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687
agable0b65e732016-11-22 09:25:46 -0800688 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000689 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000690
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000691 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000692 raise NotImplementedError() # Implement when needed
693
agable0b65e732016-11-22 09:25:46 -0800694 def IsTextFile(self):
695 """An alias to IsTestableFile for backwards compatibility."""
696 return self.IsTestableFile()
697
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700698 def OldContents(self):
699 """Returns an iterator over the lines in the old version of file.
700
Daniel Cheng2da34fe2017-03-21 20:42:12 -0700701 The old version is the file before any modifications in the user's
702 workspace, i.e. the "left hand side".
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700703
704 Contents will be empty if the file is a directory or does not exist.
705 Note: The carriage returns (LF or CR) are stripped off.
706 """
707 return self._diff_cache.GetOldContents(self.LocalPath(),
708 self._local_root).splitlines()
709
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000710 def NewContents(self):
711 """Returns an iterator over the lines in the new version of file.
712
713 The new version is the file in the user's workspace, i.e. the "right hand
714 side".
715
716 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000717 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000718 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000719 if self._cached_new_contents is None:
720 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800721 try:
722 self._cached_new_contents = gclient_utils.FileRead(
723 self.AbsoluteLocalPath(), 'rU').splitlines()
724 except IOError:
725 pass # File not found? That's fine; maybe it was deleted.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000726 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000727
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000728 def ChangedContents(self):
729 """Returns a list of tuples (line number, line text) of all new lines.
730
731 This relies on the scm diff output describing each changed code section
732 with a line of the form
733
734 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
735 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000736 if self._cached_changed_contents is not None:
737 return self._cached_changed_contents[:]
738 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000739 line_num = 0
740
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000741 for line in self.GenerateScmDiff().splitlines():
742 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
743 if m:
744 line_num = int(m.groups(1)[0])
745 continue
746 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000747 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000748 if not line.startswith('-'):
749 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000750 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000751
maruel@chromium.org5de13972009-06-10 18:16:06 +0000752 def __str__(self):
753 return self.LocalPath()
754
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000755 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000756 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000757
maruel@chromium.org58407af2011-04-12 23:15:57 +0000758
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000759class GitAffectedFile(AffectedFile):
760 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000761 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800762 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000763
nick@chromium.orgff526192013-06-10 19:30:26 +0000764 DIFF_CACHE = _GitDiffCache
765
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000766 def __init__(self, *args, **kwargs):
767 AffectedFile.__init__(self, *args, **kwargs)
768 self._server_path = None
agable0b65e732016-11-22 09:25:46 -0800769 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000770
agable0b65e732016-11-22 09:25:46 -0800771 def IsTestableFile(self):
772 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000773 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -0800774 # A deleted file is not testable.
775 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000776 else:
agable0b65e732016-11-22 09:25:46 -0800777 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
778 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000779
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000780
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000781class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000782 """Describe a change.
783
784 Used directly by the presubmit scripts to query the current change being
785 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000786
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000787 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000788 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000789 self.KEY: equivalent to tags['KEY']
790 """
791
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000792 _AFFECTED_FILES = AffectedFile
793
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000794 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000795 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000796 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000797 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000798
maruel@chromium.org58407af2011-04-12 23:15:57 +0000799 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000800 self, name, description, local_root, files, issue, patchset, author,
801 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000802 if files is None:
803 files = []
804 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000805 # Convert root into an absolute path.
806 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000807 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000808 self.issue = issue
809 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000810 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000811
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000812 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000813 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000814 self._description_without_tags = ''
815 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000816
maruel@chromium.orge085d812011-10-10 19:49:15 +0000817 assert all(
818 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
819
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000820 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000821 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000822 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
823 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000824 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000825
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000826 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000827 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000828 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000829
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000830 def DescriptionText(self):
831 """Returns the user-entered changelist description, minus tags.
832
833 Any line in the user-provided description starting with e.g. "FOO="
834 (whitespace permitted before and around) is considered a tag line. Such
835 lines are stripped out of the description this function returns.
836 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000837 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000838
839 def FullDescriptionText(self):
840 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000841 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000842
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000843 def SetDescriptionText(self, description):
844 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000845
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000846 Also updates the list of tags."""
847 self._full_description = description
848
849 # From the description text, build up a dictionary of key/value pairs
850 # plus the description minus all key/value or "tag" lines.
851 description_without_tags = []
852 self.tags = {}
853 for line in self._full_description.splitlines():
854 m = self.TAG_LINE_RE.match(line)
855 if m:
856 self.tags[m.group('key')] = m.group('value')
857 else:
858 description_without_tags.append(line)
859
860 # Change back to text and remove whitespace at end.
861 self._description_without_tags = (
862 '\n'.join(description_without_tags).rstrip())
863
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000864 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000865 """Returns the repository (checkout) root directory for this change,
866 as an absolute path.
867 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000868 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000869
870 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000871 """Return tags directly as attributes on the object."""
872 if not re.match(r"^[A-Z_]*$", attr):
873 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000874 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000875
Aaron Gablefc03e672017-05-15 14:09:42 -0700876 def BugsFromDescription(self):
877 """Returns all bugs referenced in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700878 tags = [b.strip() for b in self.tags.get('BUG', '').split(',') if b.strip()]
879 footers = git_footers.parse_footers(self._full_description).get('Bug', [])
880 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -0700881
882 def ReviewersFromDescription(self):
883 """Returns all reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700884 # We don't support a "R:" git-footer for reviewers; that is in metadata.
885 tags = [r.strip() for r in self.tags.get('R', '').split(',') if r.strip()]
886 return sorted(set(tags))
Aaron Gablefc03e672017-05-15 14:09:42 -0700887
888 def TBRsFromDescription(self):
889 """Returns all TBR reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700890 tags = [r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()]
891 # TODO(agable): Remove support for 'Tbr:' when TBRs are programmatically
892 # determined by self-CR+1s.
893 footers = git_footers.parse_footers(self._full_description).get('Tbr', [])
894 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -0700895
896 # TODO(agable): Delete these once we're sure they're unused.
897 @property
898 def BUG(self):
899 return ','.join(self.BugsFromDescription())
900 @property
901 def R(self):
902 return ','.join(self.ReviewersFromDescription())
903 @property
904 def TBR(self):
905 return ','.join(self.TBRsFromDescription())
906
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000907 def AllFiles(self, root=None):
908 """List all files under source control in the repo."""
909 raise NotImplementedError()
910
agable0b65e732016-11-22 09:25:46 -0800911 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000912 """Returns a list of AffectedFile instances for all files in the change.
913
914 Args:
915 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +0000916 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000917
918 Returns:
919 [AffectedFile(path, action), AffectedFile(path, action)]
920 """
agable0b65e732016-11-22 09:25:46 -0800921 affected = filter(file_filter, self._affected_files)
sail@chromium.org5538e022011-05-12 17:53:16 +0000922
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000923 if include_deletes:
924 return affected
Lei Zhang9611c4c2017-04-04 01:41:56 -0700925 return filter(lambda x: x.Action() != 'D', affected)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000926
agable0b65e732016-11-22 09:25:46 -0800927 def AffectedTestableFiles(self, include_deletes=None):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000928 """Return a list of the existing text files in a change."""
929 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800930 warn("AffectedTeestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000931 " is deprecated and ignored" % str(include_deletes),
932 category=DeprecationWarning,
933 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800934 return filter(lambda x: x.IsTestableFile(),
935 self.AffectedFiles(include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000936
agable0b65e732016-11-22 09:25:46 -0800937 def AffectedTextFiles(self, include_deletes=None):
938 """An alias to AffectedTestableFiles for backwards compatibility."""
939 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000940
agable0b65e732016-11-22 09:25:46 -0800941 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000942 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800943 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000944
agable0b65e732016-11-22 09:25:46 -0800945 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000946 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800947 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000948
949 def RightHandSideLines(self):
950 """An iterator over all text lines in "new" version of changed files.
951
952 Lists lines from new or modified text files in the change.
953
954 This is useful for doing line-by-line regex checks, like checking for
955 trailing whitespace.
956
957 Yields:
958 a 3 tuple:
959 the AffectedFile instance of the current file;
960 integer line number (1-based); and
961 the contents of the line as a string.
962 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000963 return _RightHandSideLinesImpl(
964 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -0800965 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000966
Jochen Eisingerd0573ec2017-04-13 10:55:06 +0200967 def OriginalOwnersFiles(self):
968 """A map from path names of affected OWNERS files to their old content."""
969 def owners_file_filter(f):
970 return 'OWNERS' in os.path.split(f.LocalPath())[1]
971 files = self.AffectedFiles(file_filter=owners_file_filter)
972 return dict([(f.LocalPath(), f.OldContents()) for f in files])
973
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000974
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000975class GitChange(Change):
976 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000977 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000978
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000979 def AllFiles(self, root=None):
980 """List all files under source control in the repo."""
981 root = root or self.RepositoryRoot()
982 return subprocess.check_output(
983 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
984
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000985
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000986def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000987 """Finds all presubmit files that apply to a given set of source files.
988
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000989 If inherit-review-settings-ok is present right under root, looks for
990 PRESUBMIT.py in directories enclosing root.
991
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000992 Args:
993 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000994 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000995
996 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000997 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000999 files = [normpath(os.path.join(root, f)) for f in files]
1000
1001 # List all the individual directories containing files.
1002 directories = set([os.path.dirname(f) for f in files])
1003
1004 # Ignore root if inherit-review-settings-ok is present.
1005 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1006 root = None
1007
1008 # Collect all unique directories that may contain PRESUBMIT.py.
1009 candidates = set()
1010 for directory in directories:
1011 while True:
1012 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001013 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001014 candidates.add(directory)
1015 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001016 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001017 parent_dir = os.path.dirname(directory)
1018 if parent_dir == directory:
1019 # We hit the system root directory.
1020 break
1021 directory = parent_dir
1022
1023 # Look for PRESUBMIT.py in all candidate directories.
1024 results = []
1025 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -07001026 try:
1027 for f in os.listdir(directory):
1028 p = os.path.join(directory, f)
1029 if os.path.isfile(p) and re.match(
1030 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1031 results.append(p)
1032 except OSError:
1033 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001034
tobiasjs2836bcf2016-08-16 04:08:16 -07001035 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001036 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001037
1038
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001039class GetTryMastersExecuter(object):
1040 @staticmethod
1041 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1042 """Executes GetPreferredTryMasters() from a single presubmit script.
1043
1044 Args:
1045 script_text: The text of the presubmit script.
1046 presubmit_path: Project script to run.
1047 project: Project name to pass to presubmit script for bot selection.
1048
1049 Return:
1050 A map of try masters to map of builders to set of tests.
1051 """
1052 context = {}
1053 try:
1054 exec script_text in context
1055 except Exception, e:
1056 raise PresubmitFailure('"%s" had an exception.\n%s'
1057 % (presubmit_path, e))
1058
1059 function_name = 'GetPreferredTryMasters'
1060 if function_name not in context:
1061 return {}
1062 get_preferred_try_masters = context[function_name]
1063 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1064 raise PresubmitFailure(
1065 'Expected function "GetPreferredTryMasters" to take two arguments.')
1066 return get_preferred_try_masters(project, change)
1067
1068
rmistry@google.com5626a922015-02-26 14:03:30 +00001069class GetPostUploadExecuter(object):
1070 @staticmethod
1071 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1072 """Executes PostUploadHook() from a single presubmit script.
1073
1074 Args:
1075 script_text: The text of the presubmit script.
1076 presubmit_path: Project script to run.
1077 cl: The Changelist object.
1078 change: The Change object.
1079
1080 Return:
1081 A list of results objects.
1082 """
1083 context = {}
1084 try:
1085 exec script_text in context
1086 except Exception, e:
1087 raise PresubmitFailure('"%s" had an exception.\n%s'
1088 % (presubmit_path, e))
1089
1090 function_name = 'PostUploadHook'
1091 if function_name not in context:
1092 return {}
1093 post_upload_hook = context[function_name]
1094 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1095 raise PresubmitFailure(
1096 'Expected function "PostUploadHook" to take three arguments.')
1097 return post_upload_hook(cl, change, OutputApi(False))
1098
1099
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001100def _MergeMasters(masters1, masters2):
1101 """Merges two master maps. Merges also the tests of each builder."""
1102 result = {}
1103 for (master, builders) in itertools.chain(masters1.iteritems(),
1104 masters2.iteritems()):
1105 new_builders = result.setdefault(master, {})
1106 for (builder, tests) in builders.iteritems():
1107 new_builders.setdefault(builder, set([])).update(tests)
1108 return result
1109
1110
1111def DoGetTryMasters(change,
1112 changed_files,
1113 repository_root,
1114 default_presubmit,
1115 project,
1116 verbose,
1117 output_stream):
1118 """Get the list of try masters from the presubmit scripts.
1119
1120 Args:
1121 changed_files: List of modified files.
1122 repository_root: The repository root.
1123 default_presubmit: A default presubmit script to execute in any case.
1124 project: Optional name of a project used in selecting trybots.
1125 verbose: Prints debug info.
1126 output_stream: A stream to write debug output to.
1127
1128 Return:
1129 Map of try masters to map of builders to set of tests.
1130 """
1131 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1132 if not presubmit_files and verbose:
1133 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1134 results = {}
1135 executer = GetTryMastersExecuter()
1136
1137 if default_presubmit:
1138 if verbose:
1139 output_stream.write("Running default presubmit script.\n")
1140 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1141 results = _MergeMasters(results, executer.ExecPresubmitScript(
1142 default_presubmit, fake_path, project, change))
1143 for filename in presubmit_files:
1144 filename = os.path.abspath(filename)
1145 if verbose:
1146 output_stream.write("Running %s\n" % filename)
1147 # Accept CRLF presubmit script.
1148 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1149 results = _MergeMasters(results, executer.ExecPresubmitScript(
1150 presubmit_script, filename, project, change))
1151
1152 # Make sets to lists again for later JSON serialization.
1153 for builders in results.itervalues():
1154 for builder in builders:
1155 builders[builder] = list(builders[builder])
1156
1157 if results and verbose:
1158 output_stream.write('%s\n' % str(results))
1159 return results
1160
1161
rmistry@google.com5626a922015-02-26 14:03:30 +00001162def DoPostUploadExecuter(change,
1163 cl,
1164 repository_root,
1165 verbose,
1166 output_stream):
1167 """Execute the post upload hook.
1168
1169 Args:
1170 change: The Change object.
1171 cl: The Changelist object.
1172 repository_root: The repository root.
1173 verbose: Prints debug info.
1174 output_stream: A stream to write debug output to.
1175 """
1176 presubmit_files = ListRelevantPresubmitFiles(
1177 change.LocalPaths(), repository_root)
1178 if not presubmit_files and verbose:
1179 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1180 results = []
1181 executer = GetPostUploadExecuter()
1182 # The root presubmit file should be executed after the ones in subdirectories.
1183 # i.e. the specific post upload hooks should run before the general ones.
1184 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1185 presubmit_files.reverse()
1186
1187 for filename in presubmit_files:
1188 filename = os.path.abspath(filename)
1189 if verbose:
1190 output_stream.write("Running %s\n" % filename)
1191 # Accept CRLF presubmit script.
1192 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1193 results.extend(executer.ExecPresubmitScript(
1194 presubmit_script, filename, cl, change))
1195 output_stream.write('\n')
1196 if results:
1197 output_stream.write('** Post Upload Hook Messages **\n')
1198 for result in results:
1199 result.handle(output_stream)
1200 output_stream.write('\n')
1201
1202 return results
1203
1204
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001205class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001206 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001207 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001208 """
1209 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001210 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001211 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001212 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001213 gerrit_obj: provides basic Gerrit codereview functionality.
1214 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001215 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001216 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001217 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001218 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001219 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001220 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001221 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001222
1223 def ExecPresubmitScript(self, script_text, presubmit_path):
1224 """Executes a single presubmit script.
1225
1226 Args:
1227 script_text: The text of the presubmit script.
1228 presubmit_path: The path to the presubmit file (this will be reported via
1229 input_api.PresubmitLocalPath()).
1230
1231 Return:
1232 A list of result objects, empty if no problems.
1233 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001234
chase@chromium.org8e416c82009-10-06 04:30:44 +00001235 # Change to the presubmit file's directory to support local imports.
1236 main_path = os.getcwd()
1237 os.chdir(os.path.dirname(presubmit_path))
1238
1239 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001240 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001241 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001242 gerrit_obj=self.gerrit, dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001243 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001244 try:
1245 exec script_text in context
1246 except Exception, e:
1247 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001248
1249 # These function names must change if we make substantial changes to
1250 # the presubmit API that are not backwards compatible.
1251 if self.committing:
1252 function_name = 'CheckChangeOnCommit'
1253 else:
1254 function_name = 'CheckChangeOnUpload'
1255 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001256 context['__args'] = (input_api, OutputApi(self.committing))
tobiasjs2836bcf2016-08-16 04:08:16 -07001257 logging.debug('Running %s in %s', function_name, presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001258 result = eval(function_name + '(*__args)', context)
tobiasjs2836bcf2016-08-16 04:08:16 -07001259 logging.debug('Running %s done.', function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001260 if not (isinstance(result, types.TupleType) or
1261 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001262 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001263 'Presubmit functions must return a tuple or list')
1264 for item in result:
1265 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001266 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001267 'All presubmit results must be of types derived from '
1268 'output_api.PresubmitResult')
1269 else:
1270 result = () # no error since the script doesn't care about current event.
1271
scottmg86099d72016-09-01 09:16:51 -07001272 input_api.ShutdownPool()
1273
chase@chromium.org8e416c82009-10-06 04:30:44 +00001274 # Return the process to the original working directory.
1275 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001276 return result
1277
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001278def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001279 committing,
1280 verbose,
1281 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001282 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001283 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001284 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001285 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001286 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001287 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001288 """Runs all presubmit checks that apply to the files in the change.
1289
1290 This finds all PRESUBMIT.py files in directories enclosing the files in the
1291 change (up to the repository root) and calls the relevant entrypoint function
1292 depending on whether the change is being committed or uploaded.
1293
1294 Prints errors, warnings and notifications. Prompts the user for warnings
1295 when needed.
1296
1297 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001298 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001299 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001300 verbose: Prints debug info.
1301 output_stream: A stream to write output from presubmit tests to.
1302 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001303 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001304 may_prompt: Enable (y/n) questions on warning or error. If False,
1305 any questions are answered with yes by default.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001306 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001307 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001308 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001309
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001310 Warning:
1311 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1312 SHOULD be sys.stdin.
1313
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001314 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001315 A PresubmitOutput object. Use output.should_continue() to figure out
1316 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001317 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001318 old_environ = os.environ
1319 try:
1320 # Make sure python subprocesses won't generate .pyc files.
1321 os.environ = os.environ.copy()
1322 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001323
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001324 output = PresubmitOutput(input_stream, output_stream)
1325 if committing:
1326 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001327 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001328 output.write("Running presubmit upload checks ...\n")
1329 start_time = time.time()
1330 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001331 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001332 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001333 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001334 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001335 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001336 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001337 if default_presubmit:
1338 if verbose:
1339 output.write("Running default presubmit script.\n")
1340 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1341 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1342 for filename in presubmit_files:
1343 filename = os.path.abspath(filename)
1344 if verbose:
1345 output.write("Running %s\n" % filename)
1346 # Accept CRLF presubmit script.
1347 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1348 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001349
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001350 errors = []
1351 notifications = []
1352 warnings = []
1353 for result in results:
1354 if result.fatal:
1355 errors.append(result)
1356 elif result.should_prompt:
1357 warnings.append(result)
1358 else:
1359 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001360
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001361 output.write('\n')
1362 for name, items in (('Messages', notifications),
1363 ('Warnings', warnings),
1364 ('ERRORS', errors)):
1365 if items:
1366 output.write('** Presubmit %s **\n' % name)
1367 for item in items:
1368 item.handle(output)
1369 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001370
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001371 total_time = time.time() - start_time
1372 if total_time > 1.0:
1373 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001374
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001375 if errors:
1376 output.fail()
1377 elif warnings:
1378 output.write('There were presubmit warnings. ')
1379 if may_prompt:
1380 output.prompt_yes_no('Are you sure you wish to continue? (y/N): ')
1381 else:
1382 output.write('Presubmit checks passed.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001383
1384 global _ASKED_FOR_FEEDBACK
1385 # Ask for feedback one time out of 5.
1386 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001387 output.write(
1388 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1389 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1390 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001391 _ASKED_FOR_FEEDBACK = True
1392 return output
1393 finally:
1394 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001395
1396
1397def ScanSubDirs(mask, recursive):
1398 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001399 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001400
1401 results = []
1402 for root, dirs, files in os.walk('.'):
1403 if '.svn' in dirs:
1404 dirs.remove('.svn')
1405 if '.git' in dirs:
1406 dirs.remove('.git')
1407 for name in files:
1408 if fnmatch.fnmatch(name, mask):
1409 results.append(os.path.join(root, name))
1410 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001411
1412
1413def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001414 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001415 files = []
1416 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001417 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001418 return files
1419
1420
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001421def load_files(options, args):
1422 """Tries to determine the SCM."""
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001423 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001424 if args:
1425 files = ParseFiles(args, options.recursive)
agable0b65e732016-11-22 09:25:46 -08001426 change_scm = scm.determine_scm(options.root)
1427 if change_scm == 'git':
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001428 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001429 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001430 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001431 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001432 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001433 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001434 if not files:
1435 return None, None
1436 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001437 return change_class, files
1438
1439
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001440class NonexistantCannedCheckFilter(Exception):
1441 pass
1442
1443
1444@contextlib.contextmanager
1445def canned_check_filter(method_names):
1446 filtered = {}
1447 try:
1448 for method_name in method_names:
1449 if not hasattr(presubmit_canned_checks, method_name):
1450 raise NonexistantCannedCheckFilter(method_name)
1451 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1452 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1453 yield
1454 finally:
1455 for name, method in filtered.iteritems():
1456 setattr(presubmit_canned_checks, name, method)
1457
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001458
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001459def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001460 """Runs an external program, potentially from a child process created by the
1461 multiprocessing module.
1462
1463 multiprocessing needs a top level function with a single argument.
1464 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001465 cmd_data.kwargs['stdout'] = subprocess.PIPE
1466 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1467 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001468 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001469 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001470 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001471 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001472 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001473 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001474 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1475 if code != 0:
1476 return cmd_data.message(
1477 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1478 if cmd_data.info:
1479 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001480
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001481
sbc@chromium.org013731e2015-02-26 18:28:43 +00001482def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001483 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001484 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001485 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001486 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001487 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1488 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001489 parser.add_option("-r", "--recursive", action="store_true",
1490 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001491 parser.add_option("-v", "--verbose", action="count", default=0,
1492 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001493 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001494 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001495 parser.add_option("--description", default='')
1496 parser.add_option("--issue", type='int', default=0)
1497 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001498 parser.add_option("--root", default=os.getcwd(),
1499 help="Search for PRESUBMIT.py up to this directory. "
1500 "If inherit-review-settings-ok is present in this "
1501 "directory, parent directories up to the root file "
1502 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001503 parser.add_option("--upstream",
1504 help="Git only: the base ref or upstream branch against "
1505 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001506 parser.add_option("--default_presubmit")
1507 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001508 parser.add_option("--skip_canned", action='append', default=[],
1509 help="A list of checks to skip which appear in "
1510 "presubmit_canned_checks. Can be provided multiple times "
1511 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001512 parser.add_option("--dry_run", action='store_true',
1513 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001514 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001515 parser.add_option("--gerrit_fetch", action='store_true',
1516 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001517 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1518 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001519 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1520 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001521 # These are for OAuth2 authentication for bots. See also apply_issue.py
1522 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1523 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1524
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001525 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001526 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001527 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001528
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001529 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001530 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001531 elif options.verbose:
1532 logging.basicConfig(level=logging.INFO)
1533 else:
1534 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001535
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001536 if (any((options.rietveld_url, options.rietveld_email_file,
1537 options.rietveld_fetch, options.rietveld_private_key_file))
1538 and any((options.gerrit_url, options.gerrit_fetch))):
1539 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1540 'allowed')
1541
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001542 if options.rietveld_email and options.rietveld_email_file:
1543 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1544 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001545 if options.rietveld_email_file:
1546 with open(options.rietveld_email_file, "rb") as f:
1547 options.rietveld_email = f.read().strip()
1548
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001549 change_class, files = load_files(options, args)
1550 if not change_class:
1551 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001552 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001553
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001554 rietveld_obj, gerrit_obj = None, None
1555
maruel@chromium.org239f4112011-06-03 20:08:23 +00001556 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001557 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001558 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001559 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1560 options.rietveld_url,
1561 options.rietveld_email,
1562 options.rietveld_private_key_file)
1563 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001564 rietveld_obj = rietveld.CachingRietveld(
1565 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001566 auth_config,
1567 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001568 if options.rietveld_fetch:
1569 assert options.issue
1570 props = rietveld_obj.get_issue_properties(options.issue, False)
1571 options.author = props['owner_email']
1572 options.description = props['description']
1573 logging.info('Got author: "%s"', options.author)
1574 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001575
1576 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001577 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001578 rietveld_obj = None
1579 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1580 options.author = gerrit_obj.GetChangeOwner(options.issue)
1581 options.description = gerrit_obj.GetChangeDescription(options.issue,
1582 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001583 logging.info('Got author: "%s"', options.author)
1584 logging.info('Got description: """\n%s\n"""', options.description)
1585
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001586 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001587 with canned_check_filter(options.skip_canned):
1588 results = DoPresubmitChecks(
1589 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001590 options.description,
1591 options.root,
1592 files,
1593 options.issue,
1594 options.patchset,
1595 options.author,
1596 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001597 options.commit,
1598 options.verbose,
1599 sys.stdout,
1600 sys.stdin,
1601 options.default_presubmit,
1602 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001603 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001604 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001605 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001606 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001607 except NonexistantCannedCheckFilter, e:
1608 print >> sys.stderr, (
1609 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1610 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001611 except PresubmitFailure, e:
1612 print >> sys.stderr, e
1613 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1614 print >> sys.stderr, 'If all fails, contact maruel@'
1615 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001616
1617
1618if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001619 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001620 try:
1621 sys.exit(main())
1622 except KeyboardInterrupt:
1623 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001624 sys.exit(2)