blob: ca54dd14b3de5226e9d56b4c7118ac61d515941c [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
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000046import gerrit_util
dpranke@chromium.org2a009622011-03-01 02:43:31 +000047import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000048import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000049import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000050import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000051import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052
53
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000054# Ask for feedback only once in program lifetime.
55_ASKED_FOR_FEEDBACK = False
56
57
maruel@chromium.org899e1c12011-04-07 17:03:18 +000058class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000059 pass
60
61
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000062class CommandData(object):
63 def __init__(self, name, cmd, kwargs, message):
64 self.name = name
65 self.cmd = cmd
66 self.kwargs = kwargs
67 self.message = message
68 self.info = None
69
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000070
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000071def normpath(path):
72 '''Version of os.path.normpath that also changes backward slashes to
73 forward slashes when not running on Windows.
74 '''
75 # This is safe to always do because the Windows version of os.path.normpath
76 # will replace forward slashes with backward slashes.
77 path = path.replace(os.sep, '/')
78 return os.path.normpath(path)
79
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000080
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000081def _RightHandSideLinesImpl(affected_files):
82 """Implements RightHandSideLines for InputApi and GclChange."""
83 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000084 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000085 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000086 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000087
88
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000089class PresubmitOutput(object):
90 def __init__(self, input_stream=None, output_stream=None):
91 self.input_stream = input_stream
92 self.output_stream = output_stream
93 self.reviewers = []
94 self.written_output = []
95 self.error_count = 0
96
97 def prompt_yes_no(self, prompt_string):
98 self.write(prompt_string)
99 if self.input_stream:
100 response = self.input_stream.readline().strip().lower()
101 if response not in ('y', 'yes'):
102 self.fail()
103 else:
104 self.fail()
105
106 def fail(self):
107 self.error_count += 1
108
109 def should_continue(self):
110 return not self.error_count
111
112 def write(self, s):
113 self.written_output.append(s)
114 if self.output_stream:
115 self.output_stream.write(s)
116
117 def getvalue(self):
118 return ''.join(self.written_output)
119
120
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000121# Top level object so multiprocessing can pickle
122# Public access through OutputApi object.
123class _PresubmitResult(object):
124 """Base class for result objects."""
125 fatal = False
126 should_prompt = False
127
128 def __init__(self, message, items=None, long_text=''):
129 """
130 message: A short one-line message to indicate errors.
131 items: A list of short strings to indicate where errors occurred.
132 long_text: multi-line text output, e.g. from another tool
133 """
134 self._message = message
135 self._items = items or []
136 if items:
137 self._items = items
138 self._long_text = long_text.rstrip()
139
140 def handle(self, output):
141 output.write(self._message)
142 output.write('\n')
143 for index, item in enumerate(self._items):
144 output.write(' ')
145 # Write separately in case it's unicode.
146 output.write(str(item))
147 if index < len(self._items) - 1:
148 output.write(' \\')
149 output.write('\n')
150 if self._long_text:
151 output.write('\n***************\n')
152 # Write separately in case it's unicode.
153 output.write(self._long_text)
154 output.write('\n***************\n')
155 if self.fatal:
156 output.fail()
157
158
159# Top level object so multiprocessing can pickle
160# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000161class _PresubmitError(_PresubmitResult):
162 """A hard presubmit error."""
163 fatal = True
164
165
166# Top level object so multiprocessing can pickle
167# Public access through OutputApi object.
168class _PresubmitPromptWarning(_PresubmitResult):
169 """An warning that prompts the user if they want to continue."""
170 should_prompt = True
171
172
173# Top level object so multiprocessing can pickle
174# Public access through OutputApi object.
175class _PresubmitNotifyResult(_PresubmitResult):
176 """Just print something to the screen -- but it's not even a warning."""
177 pass
178
179
180# Top level object so multiprocessing can pickle
181# Public access through OutputApi object.
182class _MailTextResult(_PresubmitResult):
183 """A warning that should be included in the review request email."""
184 def __init__(self, *args, **kwargs):
185 super(_MailTextResult, self).__init__()
186 raise NotImplementedError()
187
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000188class GerritAccessor(object):
189 """Limited Gerrit functionality for canned presubmit checks to work.
190
191 To avoid excessive Gerrit calls, caches the results.
192 """
193
194 def __init__(self, host):
195 self.host = host
196 self.cache = {}
197
198 def _FetchChangeDetail(self, issue):
199 # Separate function to be easily mocked in tests.
200 return gerrit_util.GetChangeDetail(
201 self.host, str(issue),
202 ['ALL_REVISIONS', 'DETAILED_LABELS'])
203
204 def GetChangeInfo(self, issue):
205 """Returns labels and all revisions (patchsets) for this issue.
206
207 The result is a dictionary according to Gerrit REST Api.
208 https://gerrit-review.googlesource.com/Documentation/rest-api.html
209
210 However, API isn't very clear what's inside, so see tests for example.
211 """
212 assert issue
213 cache_key = int(issue)
214 if cache_key not in self.cache:
215 self.cache[cache_key] = self._FetchChangeDetail(issue)
216 return self.cache[cache_key]
217
218 def GetChangeDescription(self, issue, patchset=None):
219 """If patchset is none, fetches current patchset."""
220 info = self.GetChangeInfo(issue)
221 # info is a reference to cache. We'll modify it here adding description to
222 # it to the right patchset, if it is not yet there.
223
224 # Find revision info for the patchset we want.
225 if patchset is not None:
226 for rev, rev_info in info['revisions'].iteritems():
227 if str(rev_info['_number']) == str(patchset):
228 break
229 else:
230 raise Exception('patchset %s doesn\'t exist in issue %s' % (
231 patchset, issue))
232 else:
233 rev = info['current_revision']
234 rev_info = info['revisions'][rev]
235
236 # Updates revision info, which is part of cached issue info.
237 if 'real_description' not in rev_info:
238 rev_info['real_description'] = (
239 gerrit_util.GetChangeDescriptionFromGitiles(
240 rev_info['fetch']['http']['url'], rev))
241 return rev_info['real_description']
242
243 def GetChangeOwner(self, issue):
244 return self.GetChangeInfo(issue)['owner']['email']
245
246 def GetChangeReviewers(self, issue, approving_only=True):
agable565adb52016-07-22 14:48:07 -0700247 cr = self.GetChangeInfo(issue)['labels']['Code-Review']
248 max_value = max(int(k) for k in cr['values'].keys())
249 return [r['email'] for r in cr['all']
250 if not approving_only or r.get('value', 0) == max_value]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000251
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000252
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000253class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000254 """An instance of OutputApi gets passed to presubmit scripts so that they
255 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000257 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000258 PresubmitError = _PresubmitError
259 PresubmitPromptWarning = _PresubmitPromptWarning
260 PresubmitNotifyResult = _PresubmitNotifyResult
261 MailTextResult = _MailTextResult
262
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000263 def __init__(self, is_committing):
264 self.is_committing = is_committing
265
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000266 def PresubmitPromptOrNotify(self, *args, **kwargs):
267 """Warn the user when uploading, but only notify if committing."""
268 if self.is_committing:
269 return self.PresubmitNotifyResult(*args, **kwargs)
270 return self.PresubmitPromptWarning(*args, **kwargs)
271
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000272
273class InputApi(object):
274 """An instance of this object is passed to presubmit scripts so they can
275 know stuff about the change they're looking at.
276 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000277 # Method could be a function
278 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000279
maruel@chromium.org3410d912009-06-09 20:56:16 +0000280 # File extensions that are considered source files from a style guide
281 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000282 #
283 # Files without an extension aren't included in the list. If you want to
284 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
285 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000286 DEFAULT_WHITE_LIST = (
287 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000288 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
289 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000290 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000291 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000292 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000293 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000294 )
295
296 # Path regexp that should be excluded from being considered containing source
297 # files. Don't modify this list from a presubmit script!
298 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000299 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000300 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000301 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
302 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000303 # Output directories (just in case)
304 r".*\bDebug[\\\/].*",
305 r".*\bRelease[\\\/].*",
306 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000307 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000308 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000309 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000310 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000311 r"(|.*[\\\/])\.git[\\\/].*",
312 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000313 # There is no point in processing a patch file.
314 r".+\.diff$",
315 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000316 )
317
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000318 def __init__(self, change, presubmit_path, is_committing,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000319 rietveld_obj, verbose, gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000320 """Builds an InputApi object.
321
322 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000323 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000324 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000325 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000326 rietveld_obj: rietveld.Rietveld client object
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000327 gerrit_obj: provides basic Gerrit codereview functionality.
328 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000329 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000330 # Version number of the presubmit_support script.
331 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000332 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000333 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000334 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000335 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000336 self.dry_run = dry_run
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000337 # TBD
338 self.host_url = 'http://codereview.chromium.org'
339 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000340 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000341
342 # We expose various modules and functions as attributes of the input_api
343 # so that presubmit scripts don't have to import them.
344 self.basename = os.path.basename
345 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000346 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000347 self.cStringIO = cStringIO
dcheng091b7db2016-06-16 01:27:51 -0700348 self.fnmatch = fnmatch
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000349 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000350 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000351 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000352 self.os_listdir = os.listdir
353 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000354 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000355 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000356 self.pickle = pickle
357 self.marshal = marshal
358 self.re = re
359 self.subprocess = subprocess
360 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000361 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000362 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000363 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000364 self.urllib2 = urllib2
365
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000366 # To easily fork python.
367 self.python_executable = sys.executable
368 self.environ = os.environ
369
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000370 # InputApi.platform is the platform you're currently running on.
371 self.platform = sys.platform
372
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000373 self.cpu_count = multiprocessing.cpu_count()
374
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000375 # this is done here because in RunTests, the current working directory has
376 # changed, which causes Pool() to explode fantastically when run on windows
377 # (because it tries to load the __main__ module, which imports lots of
378 # things relative to the current working directory).
379 self._run_tests_pool = multiprocessing.Pool(self.cpu_count)
380
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000381 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000382 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000383
384 # We carry the canned checks so presubmit scripts can easily use them.
385 self.canned_checks = presubmit_canned_checks
386
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000387 # TODO(dpranke): figure out a list of all approved owners for a repo
388 # in order to be able to handle wildcard OWNERS files?
389 self.owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -0700390 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000391 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000392 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000393
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000394 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000395 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000396 # Access to a protected member _XX of a client class
397 # pylint: disable=W0212
398 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000399 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000400 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
401 for (a, b, header) in cpplint._re_pattern_templates
402 ]
403
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000404 def PresubmitLocalPath(self):
405 """Returns the local path of the presubmit script currently being run.
406
407 This is useful if you don't want to hard-code absolute paths in the
408 presubmit script. For example, It can be used to find another file
409 relative to the PRESUBMIT.py script, so the whole tree can be branched and
410 the presubmit script still works, without editing its content.
411 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000412 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000413
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000414 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000415 """Translate a depot path to a local path (relative to client root).
416
417 Args:
418 Depot path as a string.
419
420 Returns:
421 The local path of the depot path under the user's current client, or None
422 if the file is not mapped.
423
424 Remember to check for the None case and show an appropriate error!
425 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000426 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
427 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000428
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000429 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000430 """Translate a local path to a depot path.
431
432 Args:
433 Local path (relative to current directory, or absolute) as a string.
434
435 Returns:
436 The depot path (SVN URL) of the file if mapped, otherwise None.
437 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000438 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
439 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000440
sail@chromium.org5538e022011-05-12 17:53:16 +0000441 def AffectedFiles(self, include_dirs=False, include_deletes=True,
442 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000443 """Same as input_api.change.AffectedFiles() except only lists files
444 (and optionally directories) in the same directory as the current presubmit
445 script, or subdirectories thereof.
446 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000447 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000448 if len(dir_with_slash) == 1:
449 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000450
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000451 return filter(
452 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000453 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000454
455 def LocalPaths(self, include_dirs=False):
456 """Returns local paths of input_api.AffectedFiles()."""
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000457 paths = [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
458 logging.debug("LocalPaths: %s", paths)
459 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000460
461 def AbsoluteLocalPaths(self, include_dirs=False):
462 """Returns absolute local paths of input_api.AffectedFiles()."""
463 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
464
465 def ServerPaths(self, include_dirs=False):
466 """Returns server paths of input_api.AffectedFiles()."""
467 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
468
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000469 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000470 """Same as input_api.change.AffectedTextFiles() except only lists files
471 in the same directory as the current presubmit script, or subdirectories
472 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000473 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000474 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000475 warn("AffectedTextFiles(include_deletes=%s)"
476 " is deprecated and ignored" % str(include_deletes),
477 category=DeprecationWarning,
478 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000479 return filter(lambda x: x.IsTextFile(),
480 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000481
maruel@chromium.org3410d912009-06-09 20:56:16 +0000482 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
483 """Filters out files that aren't considered "source file".
484
485 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
486 and InputApi.DEFAULT_BLACK_LIST is used respectively.
487
488 The lists will be compiled as regular expression and
489 AffectedFile.LocalPath() needs to pass both list.
490
491 Note: Copy-paste this function to suit your needs or use a lambda function.
492 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000493 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000494 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000495 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000496 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000497 return True
498 return False
499 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
500 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
501
502 def AffectedSourceFiles(self, source_file):
503 """Filter the list of AffectedTextFiles by the function source_file.
504
505 If source_file is None, InputApi.FilterSourceFile() is used.
506 """
507 if not source_file:
508 source_file = self.FilterSourceFile
509 return filter(source_file, self.AffectedTextFiles())
510
511 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000512 """An iterator over all text lines in "new" version of changed files.
513
514 Only lists lines from new or modified text files in the change that are
515 contained by the directory of the currently executing presubmit script.
516
517 This is useful for doing line-by-line regex checks, like checking for
518 trailing whitespace.
519
520 Yields:
521 a 3 tuple:
522 the AffectedFile instance of the current file;
523 integer line number (1-based); and
524 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000525
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000526 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000527 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000528 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000529 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000530
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000531 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000532 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000533
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000534 Deny reading anything outside the repository.
535 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000536 if isinstance(file_item, AffectedFile):
537 file_item = file_item.AbsoluteLocalPath()
538 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000539 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000540 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000541
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000542 @property
543 def tbr(self):
544 """Returns if a change is TBR'ed."""
545 return 'TBR' in self.change.tags
546
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000547 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000548 tests = []
549 msgs = []
550 for t in tests_mix:
551 if isinstance(t, OutputApi.PresubmitResult):
552 msgs.append(t)
553 else:
554 assert issubclass(t.message, _PresubmitResult)
555 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000556 if self.verbose:
557 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000558 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000559 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000560 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000561 else:
562 msgs.extend(map(CallCommand, tests))
563 return [m for m in msgs if m]
564
scottmg86099d72016-09-01 09:16:51 -0700565 def ShutdownPool(self):
566 self._run_tests_pool.close()
567 self._run_tests_pool.join()
568 self._run_tests_pool = None
569
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000570
nick@chromium.orgff526192013-06-10 19:30:26 +0000571class _DiffCache(object):
572 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000573 def __init__(self, upstream=None):
574 """Stores the upstream revision against which all diffs will be computed."""
575 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000576
577 def GetDiff(self, path, local_root):
578 """Get the diff for a particular path."""
579 raise NotImplementedError()
580
581
582class _SvnDiffCache(_DiffCache):
583 """DiffCache implementation for subversion."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000584 def __init__(self, *args, **kwargs):
585 super(_SvnDiffCache, self).__init__(*args, **kwargs)
nick@chromium.orgff526192013-06-10 19:30:26 +0000586 self._diffs_by_file = {}
587
588 def GetDiff(self, path, local_root):
589 if path not in self._diffs_by_file:
590 self._diffs_by_file[path] = scm.SVN.GenerateDiff([path], local_root,
591 False, None)
592 return self._diffs_by_file[path]
593
594
595class _GitDiffCache(_DiffCache):
596 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000597 def __init__(self, upstream):
598 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000599 self._diffs_by_file = None
600
601 def GetDiff(self, path, local_root):
602 if not self._diffs_by_file:
603 # Compute a single diff for all files and parse the output; should
604 # with git this is much faster than computing one diff for each file.
605 diffs = {}
606
607 # Don't specify any filenames below, because there are command line length
608 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000609 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
610 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000611
612 # This regex matches the path twice, separated by a space. Note that
613 # filename itself may contain spaces.
614 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
615 current_diff = []
616 keep_line_endings = True
617 for x in unified_diff.splitlines(keep_line_endings):
618 match = file_marker.match(x)
619 if match:
620 # Marks the start of a new per-file section.
621 diffs[match.group('filename')] = current_diff = [x]
622 elif x.startswith('diff --git'):
623 raise PresubmitFailure('Unexpected diff line: %s' % x)
624 else:
625 current_diff.append(x)
626
627 self._diffs_by_file = dict(
628 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
629
630 if path not in self._diffs_by_file:
631 raise PresubmitFailure(
632 'Unified diff did not contain entry for file %s' % path)
633
634 return self._diffs_by_file[path]
635
636
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000637class AffectedFile(object):
638 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000639
640 DIFF_CACHE = _DiffCache
641
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000642 # Method could be a function
643 # pylint: disable=R0201
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000644 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000645 self._path = path
646 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000647 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000648 self._is_directory = None
649 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000650 self._cached_changed_contents = None
651 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000652 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700653 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000654
655 def ServerPath(self):
656 """Returns a path string that identifies the file in the SCM system.
657
658 Returns the empty string if the file does not exist in SCM.
659 """
nick@chromium.orgff526192013-06-10 19:30:26 +0000660 return ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000661
662 def LocalPath(self):
663 """Returns the path of this file on the local disk relative to client root.
664 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000665 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000666
667 def AbsoluteLocalPath(self):
668 """Returns the absolute path of this file on the local disk.
669 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000670 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000671
672 def IsDirectory(self):
673 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000674 if self._is_directory is None:
675 path = self.AbsoluteLocalPath()
676 self._is_directory = (os.path.exists(path) and
677 os.path.isdir(path))
678 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000679
680 def Action(self):
681 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000682 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
683 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000684 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000685
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000686 def Property(self, property_name):
687 """Returns the specified SCM property of this file, or None if no such
688 property.
689 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000690 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000691
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000692 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000693 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000694
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000695 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000696 raise NotImplementedError() # Implement when needed
697
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000698 def NewContents(self):
699 """Returns an iterator over the lines in the new version of file.
700
701 The new version is the file in the user's workspace, i.e. the "right hand
702 side".
703
704 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000705 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000706 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000707 if self._cached_new_contents is None:
708 self._cached_new_contents = []
709 if not self.IsDirectory():
710 try:
711 self._cached_new_contents = gclient_utils.FileRead(
712 self.AbsoluteLocalPath(), 'rU').splitlines()
713 except IOError:
714 pass # File not found? That's fine; maybe it was deleted.
715 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000716
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000717 def ChangedContents(self):
718 """Returns a list of tuples (line number, line text) of all new lines.
719
720 This relies on the scm diff output describing each changed code section
721 with a line of the form
722
723 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
724 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000725 if self._cached_changed_contents is not None:
726 return self._cached_changed_contents[:]
727 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000728 line_num = 0
729
730 if self.IsDirectory():
731 return []
732
733 for line in self.GenerateScmDiff().splitlines():
734 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
735 if m:
736 line_num = int(m.groups(1)[0])
737 continue
738 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000739 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000740 if not line.startswith('-'):
741 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000742 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000743
maruel@chromium.org5de13972009-06-10 18:16:06 +0000744 def __str__(self):
745 return self.LocalPath()
746
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000747 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000748 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000749
maruel@chromium.org58407af2011-04-12 23:15:57 +0000750
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000751class SvnAffectedFile(AffectedFile):
752 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000753 # Method 'NNN' is abstract in class 'NNN' but is not overridden
754 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000755
nick@chromium.orgff526192013-06-10 19:30:26 +0000756 DIFF_CACHE = _SvnDiffCache
757
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000758 def __init__(self, *args, **kwargs):
759 AffectedFile.__init__(self, *args, **kwargs)
760 self._server_path = None
761 self._is_text_file = None
762
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000763 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000764 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000765 self._server_path = scm.SVN.CaptureLocalInfo(
766 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000767 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000768
769 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000770 if self._is_directory is None:
771 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000772 if os.path.exists(path):
773 # Retrieve directly from the file system; it is much faster than
774 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000775 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000776 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000777 self._is_directory = scm.SVN.CaptureLocalInfo(
778 [self.LocalPath()], self._local_root
779 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000780 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000781
782 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000783 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000784 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000785 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000786 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000787
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000788 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000789 if self._is_text_file is None:
790 if self.Action() == 'D':
791 # A deleted file is not a text file.
792 self._is_text_file = False
793 elif self.IsDirectory():
794 self._is_text_file = False
795 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000796 mime_type = scm.SVN.GetFileProperty(
797 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000798 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
799 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000800
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000801
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000802class GitAffectedFile(AffectedFile):
803 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000804 # Method 'NNN' is abstract in class 'NNN' but is not overridden
805 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000806
nick@chromium.orgff526192013-06-10 19:30:26 +0000807 DIFF_CACHE = _GitDiffCache
808
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000809 def __init__(self, *args, **kwargs):
810 AffectedFile.__init__(self, *args, **kwargs)
811 self._server_path = None
812 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000813
814 def ServerPath(self):
815 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000816 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000817 return self._server_path
818
819 def IsDirectory(self):
820 if self._is_directory is None:
821 path = self.AbsoluteLocalPath()
822 if os.path.exists(path):
823 # Retrieve directly from the file system; it is much faster than
824 # querying subversion, especially on Windows.
825 self._is_directory = os.path.isdir(path)
826 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000827 self._is_directory = False
828 return self._is_directory
829
830 def Property(self, property_name):
831 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000832 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000833 return self._properties[property_name]
834
835 def IsTextFile(self):
836 if self._is_text_file is None:
837 if self.Action() == 'D':
838 # A deleted file is not a text file.
839 self._is_text_file = False
840 elif self.IsDirectory():
841 self._is_text_file = False
842 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000843 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
844 return self._is_text_file
845
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000846
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000847class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000848 """Describe a change.
849
850 Used directly by the presubmit scripts to query the current change being
851 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000852
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000853 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000854 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000855 self.KEY: equivalent to tags['KEY']
856 """
857
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000858 _AFFECTED_FILES = AffectedFile
859
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000860 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000861 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000862 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000863 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000864
maruel@chromium.org58407af2011-04-12 23:15:57 +0000865 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000866 self, name, description, local_root, files, issue, patchset, author,
867 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000868 if files is None:
869 files = []
870 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000871 # Convert root into an absolute path.
872 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000873 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000874 self.issue = issue
875 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000876 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000877
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000878 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000879 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000880 self._description_without_tags = ''
881 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000882
maruel@chromium.orge085d812011-10-10 19:49:15 +0000883 assert all(
884 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
885
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000886 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000887 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000888 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
889 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000890 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000891
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000892 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000893 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000894 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000895
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000896 def DescriptionText(self):
897 """Returns the user-entered changelist description, minus tags.
898
899 Any line in the user-provided description starting with e.g. "FOO="
900 (whitespace permitted before and around) is considered a tag line. Such
901 lines are stripped out of the description this function returns.
902 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000903 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000904
905 def FullDescriptionText(self):
906 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000907 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000908
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000909 def SetDescriptionText(self, description):
910 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000911
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000912 Also updates the list of tags."""
913 self._full_description = description
914
915 # From the description text, build up a dictionary of key/value pairs
916 # plus the description minus all key/value or "tag" lines.
917 description_without_tags = []
918 self.tags = {}
919 for line in self._full_description.splitlines():
920 m = self.TAG_LINE_RE.match(line)
921 if m:
922 self.tags[m.group('key')] = m.group('value')
923 else:
924 description_without_tags.append(line)
925
926 # Change back to text and remove whitespace at end.
927 self._description_without_tags = (
928 '\n'.join(description_without_tags).rstrip())
929
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000930 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000931 """Returns the repository (checkout) root directory for this change,
932 as an absolute path.
933 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000934 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000935
936 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000937 """Return tags directly as attributes on the object."""
938 if not re.match(r"^[A-Z_]*$", attr):
939 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000940 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000941
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000942 def AllFiles(self, root=None):
943 """List all files under source control in the repo."""
944 raise NotImplementedError()
945
sail@chromium.org5538e022011-05-12 17:53:16 +0000946 def AffectedFiles(self, include_dirs=False, include_deletes=True,
947 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000948 """Returns a list of AffectedFile instances for all files in the change.
949
950 Args:
951 include_deletes: If false, deleted files will be filtered out.
952 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000953 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000954
955 Returns:
956 [AffectedFile(path, action), AffectedFile(path, action)]
957 """
958 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000959 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000960 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000961 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000962
sail@chromium.org5538e022011-05-12 17:53:16 +0000963 affected = filter(file_filter, affected)
964
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000965 if include_deletes:
966 return affected
967 else:
968 return filter(lambda x: x.Action() != 'D', affected)
969
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000970 def AffectedTextFiles(self, include_deletes=None):
971 """Return a list of the existing text files in a change."""
972 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000973 warn("AffectedTextFiles(include_deletes=%s)"
974 " is deprecated and ignored" % str(include_deletes),
975 category=DeprecationWarning,
976 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000977 return filter(lambda x: x.IsTextFile(),
978 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000979
980 def LocalPaths(self, include_dirs=False):
981 """Convenience function."""
982 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
983
984 def AbsoluteLocalPaths(self, include_dirs=False):
985 """Convenience function."""
986 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
987
988 def ServerPaths(self, include_dirs=False):
989 """Convenience function."""
990 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
991
992 def RightHandSideLines(self):
993 """An iterator over all text lines in "new" version of changed files.
994
995 Lists lines from new or modified text files in the change.
996
997 This is useful for doing line-by-line regex checks, like checking for
998 trailing whitespace.
999
1000 Yields:
1001 a 3 tuple:
1002 the AffectedFile instance of the current file;
1003 integer line number (1-based); and
1004 the contents of the line as a string.
1005 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001006 return _RightHandSideLinesImpl(
1007 x for x in self.AffectedFiles(include_deletes=False)
1008 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001009
1010
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001011class SvnChange(Change):
1012 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001013 scm = 'svn'
1014 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +00001015
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001016 def AllFiles(self, root=None):
1017 """List all files under source control in the repo."""
1018 root = root or self.RepositoryRoot()
1019 return subprocess.check_output(
1020 ['svn', 'ls', '-R', '.'], cwd=root).splitlines()
1021
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001022
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001023class GitChange(Change):
1024 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001025 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001026
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001027 def AllFiles(self, root=None):
1028 """List all files under source control in the repo."""
1029 root = root or self.RepositoryRoot()
1030 return subprocess.check_output(
1031 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
1032
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001033
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001034def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001035 """Finds all presubmit files that apply to a given set of source files.
1036
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001037 If inherit-review-settings-ok is present right under root, looks for
1038 PRESUBMIT.py in directories enclosing root.
1039
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001040 Args:
1041 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001042 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001043
1044 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001045 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001046 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001047 files = [normpath(os.path.join(root, f)) for f in files]
1048
1049 # List all the individual directories containing files.
1050 directories = set([os.path.dirname(f) for f in files])
1051
1052 # Ignore root if inherit-review-settings-ok is present.
1053 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1054 root = None
1055
1056 # Collect all unique directories that may contain PRESUBMIT.py.
1057 candidates = set()
1058 for directory in directories:
1059 while True:
1060 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001061 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001062 candidates.add(directory)
1063 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001064 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001065 parent_dir = os.path.dirname(directory)
1066 if parent_dir == directory:
1067 # We hit the system root directory.
1068 break
1069 directory = parent_dir
1070
1071 # Look for PRESUBMIT.py in all candidate directories.
1072 results = []
1073 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -07001074 try:
1075 for f in os.listdir(directory):
1076 p = os.path.join(directory, f)
1077 if os.path.isfile(p) and re.match(
1078 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1079 results.append(p)
1080 except OSError:
1081 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001082
tobiasjs2836bcf2016-08-16 04:08:16 -07001083 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001084 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001085
1086
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001087class GetTryMastersExecuter(object):
1088 @staticmethod
1089 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1090 """Executes GetPreferredTryMasters() from a single presubmit script.
1091
1092 Args:
1093 script_text: The text of the presubmit script.
1094 presubmit_path: Project script to run.
1095 project: Project name to pass to presubmit script for bot selection.
1096
1097 Return:
1098 A map of try masters to map of builders to set of tests.
1099 """
1100 context = {}
1101 try:
1102 exec script_text in context
1103 except Exception, e:
1104 raise PresubmitFailure('"%s" had an exception.\n%s'
1105 % (presubmit_path, e))
1106
1107 function_name = 'GetPreferredTryMasters'
1108 if function_name not in context:
1109 return {}
1110 get_preferred_try_masters = context[function_name]
1111 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1112 raise PresubmitFailure(
1113 'Expected function "GetPreferredTryMasters" to take two arguments.')
1114 return get_preferred_try_masters(project, change)
1115
1116
rmistry@google.com5626a922015-02-26 14:03:30 +00001117class GetPostUploadExecuter(object):
1118 @staticmethod
1119 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1120 """Executes PostUploadHook() from a single presubmit script.
1121
1122 Args:
1123 script_text: The text of the presubmit script.
1124 presubmit_path: Project script to run.
1125 cl: The Changelist object.
1126 change: The Change object.
1127
1128 Return:
1129 A list of results objects.
1130 """
1131 context = {}
1132 try:
1133 exec script_text in context
1134 except Exception, e:
1135 raise PresubmitFailure('"%s" had an exception.\n%s'
1136 % (presubmit_path, e))
1137
1138 function_name = 'PostUploadHook'
1139 if function_name not in context:
1140 return {}
1141 post_upload_hook = context[function_name]
1142 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1143 raise PresubmitFailure(
1144 'Expected function "PostUploadHook" to take three arguments.')
1145 return post_upload_hook(cl, change, OutputApi(False))
1146
1147
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001148def _MergeMasters(masters1, masters2):
1149 """Merges two master maps. Merges also the tests of each builder."""
1150 result = {}
1151 for (master, builders) in itertools.chain(masters1.iteritems(),
1152 masters2.iteritems()):
1153 new_builders = result.setdefault(master, {})
1154 for (builder, tests) in builders.iteritems():
1155 new_builders.setdefault(builder, set([])).update(tests)
1156 return result
1157
1158
1159def DoGetTryMasters(change,
1160 changed_files,
1161 repository_root,
1162 default_presubmit,
1163 project,
1164 verbose,
1165 output_stream):
1166 """Get the list of try masters from the presubmit scripts.
1167
1168 Args:
1169 changed_files: List of modified files.
1170 repository_root: The repository root.
1171 default_presubmit: A default presubmit script to execute in any case.
1172 project: Optional name of a project used in selecting trybots.
1173 verbose: Prints debug info.
1174 output_stream: A stream to write debug output to.
1175
1176 Return:
1177 Map of try masters to map of builders to set of tests.
1178 """
1179 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1180 if not presubmit_files and verbose:
1181 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1182 results = {}
1183 executer = GetTryMastersExecuter()
1184
1185 if default_presubmit:
1186 if verbose:
1187 output_stream.write("Running default presubmit script.\n")
1188 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1189 results = _MergeMasters(results, executer.ExecPresubmitScript(
1190 default_presubmit, fake_path, project, change))
1191 for filename in presubmit_files:
1192 filename = os.path.abspath(filename)
1193 if verbose:
1194 output_stream.write("Running %s\n" % filename)
1195 # Accept CRLF presubmit script.
1196 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1197 results = _MergeMasters(results, executer.ExecPresubmitScript(
1198 presubmit_script, filename, project, change))
1199
1200 # Make sets to lists again for later JSON serialization.
1201 for builders in results.itervalues():
1202 for builder in builders:
1203 builders[builder] = list(builders[builder])
1204
1205 if results and verbose:
1206 output_stream.write('%s\n' % str(results))
1207 return results
1208
1209
rmistry@google.com5626a922015-02-26 14:03:30 +00001210def DoPostUploadExecuter(change,
1211 cl,
1212 repository_root,
1213 verbose,
1214 output_stream):
1215 """Execute the post upload hook.
1216
1217 Args:
1218 change: The Change object.
1219 cl: The Changelist object.
1220 repository_root: The repository root.
1221 verbose: Prints debug info.
1222 output_stream: A stream to write debug output to.
1223 """
1224 presubmit_files = ListRelevantPresubmitFiles(
1225 change.LocalPaths(), repository_root)
1226 if not presubmit_files and verbose:
1227 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1228 results = []
1229 executer = GetPostUploadExecuter()
1230 # The root presubmit file should be executed after the ones in subdirectories.
1231 # i.e. the specific post upload hooks should run before the general ones.
1232 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1233 presubmit_files.reverse()
1234
1235 for filename in presubmit_files:
1236 filename = os.path.abspath(filename)
1237 if verbose:
1238 output_stream.write("Running %s\n" % filename)
1239 # Accept CRLF presubmit script.
1240 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1241 results.extend(executer.ExecPresubmitScript(
1242 presubmit_script, filename, cl, change))
1243 output_stream.write('\n')
1244 if results:
1245 output_stream.write('** Post Upload Hook Messages **\n')
1246 for result in results:
1247 result.handle(output_stream)
1248 output_stream.write('\n')
1249
1250 return results
1251
1252
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001253class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001254 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001255 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001256 """
1257 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001258 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001259 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001260 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001261 gerrit_obj: provides basic Gerrit codereview functionality.
1262 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001263 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001264 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001265 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001266 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001267 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001268 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001269 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001270
1271 def ExecPresubmitScript(self, script_text, presubmit_path):
1272 """Executes a single presubmit script.
1273
1274 Args:
1275 script_text: The text of the presubmit script.
1276 presubmit_path: The path to the presubmit file (this will be reported via
1277 input_api.PresubmitLocalPath()).
1278
1279 Return:
1280 A list of result objects, empty if no problems.
1281 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001282
chase@chromium.org8e416c82009-10-06 04:30:44 +00001283 # Change to the presubmit file's directory to support local imports.
1284 main_path = os.getcwd()
1285 os.chdir(os.path.dirname(presubmit_path))
1286
1287 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001288 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001289 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001290 gerrit_obj=self.gerrit, dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001291 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001292 try:
1293 exec script_text in context
1294 except Exception, e:
1295 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001296
1297 # These function names must change if we make substantial changes to
1298 # the presubmit API that are not backwards compatible.
1299 if self.committing:
1300 function_name = 'CheckChangeOnCommit'
1301 else:
1302 function_name = 'CheckChangeOnUpload'
1303 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001304 context['__args'] = (input_api, OutputApi(self.committing))
tobiasjs2836bcf2016-08-16 04:08:16 -07001305 logging.debug('Running %s in %s', function_name, presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001306 result = eval(function_name + '(*__args)', context)
tobiasjs2836bcf2016-08-16 04:08:16 -07001307 logging.debug('Running %s done.', function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001308 if not (isinstance(result, types.TupleType) or
1309 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001310 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001311 'Presubmit functions must return a tuple or list')
1312 for item in result:
1313 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001314 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001315 'All presubmit results must be of types derived from '
1316 'output_api.PresubmitResult')
1317 else:
1318 result = () # no error since the script doesn't care about current event.
1319
scottmg86099d72016-09-01 09:16:51 -07001320 input_api.ShutdownPool()
1321
chase@chromium.org8e416c82009-10-06 04:30:44 +00001322 # Return the process to the original working directory.
1323 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001324 return result
1325
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001326
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001327def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001328 committing,
1329 verbose,
1330 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001331 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001332 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001333 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001334 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001335 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001336 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001337 """Runs all presubmit checks that apply to the files in the change.
1338
1339 This finds all PRESUBMIT.py files in directories enclosing the files in the
1340 change (up to the repository root) and calls the relevant entrypoint function
1341 depending on whether the change is being committed or uploaded.
1342
1343 Prints errors, warnings and notifications. Prompts the user for warnings
1344 when needed.
1345
1346 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001347 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001348 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001349 verbose: Prints debug info.
1350 output_stream: A stream to write output from presubmit tests to.
1351 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001352 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001353 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001354 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001355 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001356 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001357
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001358 Warning:
1359 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1360 SHOULD be sys.stdin.
1361
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001362 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001363 A PresubmitOutput object. Use output.should_continue() to figure out
1364 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001365 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001366 old_environ = os.environ
1367 try:
1368 # Make sure python subprocesses won't generate .pyc files.
1369 os.environ = os.environ.copy()
1370 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001371
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001372 output = PresubmitOutput(input_stream, output_stream)
1373 if committing:
1374 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001375 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001376 output.write("Running presubmit upload checks ...\n")
1377 start_time = time.time()
1378 presubmit_files = ListRelevantPresubmitFiles(
1379 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1380 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001381 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001382 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001383 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001384 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001385 if default_presubmit:
1386 if verbose:
1387 output.write("Running default presubmit script.\n")
1388 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1389 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1390 for filename in presubmit_files:
1391 filename = os.path.abspath(filename)
1392 if verbose:
1393 output.write("Running %s\n" % filename)
1394 # Accept CRLF presubmit script.
1395 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1396 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001397
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001398 errors = []
1399 notifications = []
1400 warnings = []
1401 for result in results:
1402 if result.fatal:
1403 errors.append(result)
1404 elif result.should_prompt:
1405 warnings.append(result)
1406 else:
1407 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001408
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001409 output.write('\n')
1410 for name, items in (('Messages', notifications),
1411 ('Warnings', warnings),
1412 ('ERRORS', errors)):
1413 if items:
1414 output.write('** Presubmit %s **\n' % name)
1415 for item in items:
1416 item.handle(output)
1417 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001418
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001419 total_time = time.time() - start_time
1420 if total_time > 1.0:
1421 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001422
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001423 if not errors:
1424 if not warnings:
1425 output.write('Presubmit checks passed.\n')
1426 elif may_prompt:
1427 output.prompt_yes_no('There were presubmit warnings. '
1428 'Are you sure you wish to continue? (y/N): ')
1429 else:
1430 output.fail()
1431
1432 global _ASKED_FOR_FEEDBACK
1433 # Ask for feedback one time out of 5.
1434 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001435 output.write(
1436 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1437 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1438 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001439 _ASKED_FOR_FEEDBACK = True
1440 return output
1441 finally:
1442 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001443
1444
1445def ScanSubDirs(mask, recursive):
1446 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001447 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001448 else:
1449 results = []
1450 for root, dirs, files in os.walk('.'):
1451 if '.svn' in dirs:
1452 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001453 if '.git' in dirs:
1454 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001455 for name in files:
1456 if fnmatch.fnmatch(name, mask):
1457 results.append(os.path.join(root, name))
1458 return results
1459
1460
1461def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001462 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001463 files = []
1464 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001465 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001466 return files
1467
1468
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001469def load_files(options, args):
1470 """Tries to determine the SCM."""
1471 change_scm = scm.determine_scm(options.root)
1472 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001473 if args:
1474 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001475 if change_scm == 'svn':
1476 change_class = SvnChange
1477 if not files:
1478 files = scm.SVN.CaptureStatus([], options.root)
1479 elif change_scm == 'git':
1480 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001481 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001482 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001483 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001484 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001485 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001486 if not files:
1487 return None, None
1488 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001489 return change_class, files
1490
1491
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001492class NonexistantCannedCheckFilter(Exception):
1493 pass
1494
1495
1496@contextlib.contextmanager
1497def canned_check_filter(method_names):
1498 filtered = {}
1499 try:
1500 for method_name in method_names:
1501 if not hasattr(presubmit_canned_checks, method_name):
1502 raise NonexistantCannedCheckFilter(method_name)
1503 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1504 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1505 yield
1506 finally:
1507 for name, method in filtered.iteritems():
1508 setattr(presubmit_canned_checks, name, method)
1509
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001510
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001511def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001512 """Runs an external program, potentially from a child process created by the
1513 multiprocessing module.
1514
1515 multiprocessing needs a top level function with a single argument.
1516 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001517 cmd_data.kwargs['stdout'] = subprocess.PIPE
1518 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1519 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001520 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001521 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001522 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001523 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001524 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001525 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001526 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1527 if code != 0:
1528 return cmd_data.message(
1529 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1530 if cmd_data.info:
1531 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001532
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001533
sbc@chromium.org013731e2015-02-26 18:28:43 +00001534def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001535 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001536 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001537 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001538 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001539 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1540 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001541 parser.add_option("-r", "--recursive", action="store_true",
1542 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001543 parser.add_option("-v", "--verbose", action="count", default=0,
1544 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001545 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001546 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001547 parser.add_option("--description", default='')
1548 parser.add_option("--issue", type='int', default=0)
1549 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001550 parser.add_option("--root", default=os.getcwd(),
1551 help="Search for PRESUBMIT.py up to this directory. "
1552 "If inherit-review-settings-ok is present in this "
1553 "directory, parent directories up to the root file "
1554 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001555 parser.add_option("--upstream",
1556 help="Git only: the base ref or upstream branch against "
1557 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001558 parser.add_option("--default_presubmit")
1559 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001560 parser.add_option("--skip_canned", action='append', default=[],
1561 help="A list of checks to skip which appear in "
1562 "presubmit_canned_checks. Can be provided multiple times "
1563 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001564 parser.add_option("--dry_run", action='store_true',
1565 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001566 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001567 parser.add_option("--gerrit_fetch", action='store_true',
1568 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001569 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1570 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001571 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1572 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001573 # These are for OAuth2 authentication for bots. See also apply_issue.py
1574 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1575 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1576
phajdan.jr@chromium.org2ff30182016-03-23 09:52:51 +00001577 # TODO(phajdan.jr): Update callers and remove obsolete --trybot-json .
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001578 parser.add_option("--trybot-json",
1579 help="Output trybot information to the file specified.")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001580 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001581 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001582 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001583
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001584 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001585 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001586 elif options.verbose:
1587 logging.basicConfig(level=logging.INFO)
1588 else:
1589 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001590
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001591 if (any((options.rietveld_url, options.rietveld_email_file,
1592 options.rietveld_fetch, options.rietveld_private_key_file))
1593 and any((options.gerrit_url, options.gerrit_fetch))):
1594 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1595 'allowed')
1596
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001597 if options.rietveld_email and options.rietveld_email_file:
1598 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1599 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001600 if options.rietveld_email_file:
1601 with open(options.rietveld_email_file, "rb") as f:
1602 options.rietveld_email = f.read().strip()
1603
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001604 change_class, files = load_files(options, args)
1605 if not change_class:
1606 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001607 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001608
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001609 rietveld_obj, gerrit_obj = None, None
1610
maruel@chromium.org239f4112011-06-03 20:08:23 +00001611 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001612 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001613 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001614 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1615 options.rietveld_url,
1616 options.rietveld_email,
1617 options.rietveld_private_key_file)
1618 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001619 rietveld_obj = rietveld.CachingRietveld(
1620 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001621 auth_config,
1622 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001623 if options.rietveld_fetch:
1624 assert options.issue
1625 props = rietveld_obj.get_issue_properties(options.issue, False)
1626 options.author = props['owner_email']
1627 options.description = props['description']
1628 logging.info('Got author: "%s"', options.author)
1629 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001630
1631 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001632 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001633 rietveld_obj = None
1634 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1635 options.author = gerrit_obj.GetChangeOwner(options.issue)
1636 options.description = gerrit_obj.GetChangeDescription(options.issue,
1637 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001638 logging.info('Got author: "%s"', options.author)
1639 logging.info('Got description: """\n%s\n"""', options.description)
1640
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001641 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001642 with canned_check_filter(options.skip_canned):
1643 results = DoPresubmitChecks(
1644 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001645 options.description,
1646 options.root,
1647 files,
1648 options.issue,
1649 options.patchset,
1650 options.author,
1651 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001652 options.commit,
1653 options.verbose,
1654 sys.stdout,
1655 sys.stdin,
1656 options.default_presubmit,
1657 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001658 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001659 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001660 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001661 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001662 except NonexistantCannedCheckFilter, e:
1663 print >> sys.stderr, (
1664 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1665 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001666 except PresubmitFailure, e:
1667 print >> sys.stderr, e
1668 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1669 print >> sys.stderr, 'If all fails, contact maruel@'
1670 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001671
1672
1673if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001674 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001675 try:
1676 sys.exit(main())
1677 except KeyboardInterrupt:
1678 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001679 sys.exit(2)