blob: 0af88e55607f81c047f3d4e81d9892af6937eb5c [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
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +090015import ast # Exposed through the API.
enne@chromium.orge72c5f52013-04-16 00:36:40 +000016import cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000017import cPickle # Exposed through the API.
18import cStringIO # Exposed through the API.
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +000019import contextlib
dcheng091b7db2016-06-16 01:27:51 -070020import fnmatch # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000021import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000022import inspect
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +000023import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000024import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000025import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import marshal # Exposed through the API.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000027import multiprocessing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000028import optparse
29import os # Somewhat exposed through the API.
30import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000031import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import re # Exposed through the API.
Aaron Gable873c28d2017-09-05 16:19:04 -070033import signal
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034import sys # Parts exposed through API.
35import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000036import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000037import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000038import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000039import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000040import urllib2 # Exposed through the API.
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000041import urlparse
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000042from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000043
44# Local imports.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000045import auth
maruel@chromium.org35625c72011-03-23 17:34:02 +000046import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000047import gclient_utils
Aaron Gableb584c4f2017-04-26 16:28:08 -070048import git_footers
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000049import gerrit_util
dpranke@chromium.org2a009622011-03-01 02:43:31 +000050import owners
Jochen Eisinger76f5fc62017-04-07 16:27:46 +020051import owners_finder
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000053import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000054import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000055import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000056
57
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000058# Ask for feedback only once in program lifetime.
59_ASKED_FOR_FEEDBACK = False
60
61
maruel@chromium.org899e1c12011-04-07 17:03:18 +000062class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000063 pass
64
65
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000066class CommandData(object):
67 def __init__(self, name, cmd, kwargs, message):
68 self.name = name
69 self.cmd = cmd
70 self.kwargs = kwargs
71 self.message = message
72 self.info = None
73
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000074
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000075def normpath(path):
76 '''Version of os.path.normpath that also changes backward slashes to
77 forward slashes when not running on Windows.
78 '''
79 # This is safe to always do because the Windows version of os.path.normpath
80 # will replace forward slashes with backward slashes.
81 path = path.replace(os.sep, '/')
82 return os.path.normpath(path)
83
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000084
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000085def _RightHandSideLinesImpl(affected_files):
86 """Implements RightHandSideLines for InputApi and GclChange."""
87 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000088 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000089 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000090 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000091
92
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000093class PresubmitOutput(object):
94 def __init__(self, input_stream=None, output_stream=None):
95 self.input_stream = input_stream
96 self.output_stream = output_stream
97 self.reviewers = []
98 self.written_output = []
99 self.error_count = 0
100
101 def prompt_yes_no(self, prompt_string):
102 self.write(prompt_string)
103 if self.input_stream:
104 response = self.input_stream.readline().strip().lower()
105 if response not in ('y', 'yes'):
106 self.fail()
107 else:
108 self.fail()
109
110 def fail(self):
111 self.error_count += 1
112
113 def should_continue(self):
114 return not self.error_count
115
116 def write(self, s):
117 self.written_output.append(s)
118 if self.output_stream:
119 self.output_stream.write(s)
120
121 def getvalue(self):
122 return ''.join(self.written_output)
123
124
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000125# Top level object so multiprocessing can pickle
126# Public access through OutputApi object.
127class _PresubmitResult(object):
128 """Base class for result objects."""
129 fatal = False
130 should_prompt = False
131
132 def __init__(self, message, items=None, long_text=''):
133 """
134 message: A short one-line message to indicate errors.
135 items: A list of short strings to indicate where errors occurred.
136 long_text: multi-line text output, e.g. from another tool
137 """
138 self._message = message
139 self._items = items or []
140 if items:
141 self._items = items
142 self._long_text = long_text.rstrip()
143
144 def handle(self, output):
145 output.write(self._message)
146 output.write('\n')
147 for index, item in enumerate(self._items):
148 output.write(' ')
149 # Write separately in case it's unicode.
150 output.write(str(item))
151 if index < len(self._items) - 1:
152 output.write(' \\')
153 output.write('\n')
154 if self._long_text:
155 output.write('\n***************\n')
156 # Write separately in case it's unicode.
157 output.write(self._long_text)
158 output.write('\n***************\n')
159 if self.fatal:
160 output.fail()
161
162
163# Top level object so multiprocessing can pickle
164# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000165class _PresubmitError(_PresubmitResult):
166 """A hard presubmit error."""
167 fatal = True
168
169
170# Top level object so multiprocessing can pickle
171# Public access through OutputApi object.
172class _PresubmitPromptWarning(_PresubmitResult):
173 """An warning that prompts the user if they want to continue."""
174 should_prompt = True
175
176
177# Top level object so multiprocessing can pickle
178# Public access through OutputApi object.
179class _PresubmitNotifyResult(_PresubmitResult):
180 """Just print something to the screen -- but it's not even a warning."""
181 pass
182
183
184# Top level object so multiprocessing can pickle
185# Public access through OutputApi object.
186class _MailTextResult(_PresubmitResult):
187 """A warning that should be included in the review request email."""
188 def __init__(self, *args, **kwargs):
189 super(_MailTextResult, self).__init__()
190 raise NotImplementedError()
191
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000192class GerritAccessor(object):
193 """Limited Gerrit functionality for canned presubmit checks to work.
194
195 To avoid excessive Gerrit calls, caches the results.
196 """
197
198 def __init__(self, host):
199 self.host = host
200 self.cache = {}
201
202 def _FetchChangeDetail(self, issue):
203 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100204 try:
205 return gerrit_util.GetChangeDetail(
206 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700207 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100208 except gerrit_util.GerritError as e:
209 if e.http_status == 404:
210 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
211 'no credentials to fetch issue details' % issue)
212 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000213
214 def GetChangeInfo(self, issue):
215 """Returns labels and all revisions (patchsets) for this issue.
216
217 The result is a dictionary according to Gerrit REST Api.
218 https://gerrit-review.googlesource.com/Documentation/rest-api.html
219
220 However, API isn't very clear what's inside, so see tests for example.
221 """
222 assert issue
223 cache_key = int(issue)
224 if cache_key not in self.cache:
225 self.cache[cache_key] = self._FetchChangeDetail(issue)
226 return self.cache[cache_key]
227
228 def GetChangeDescription(self, issue, patchset=None):
229 """If patchset is none, fetches current patchset."""
230 info = self.GetChangeInfo(issue)
231 # info is a reference to cache. We'll modify it here adding description to
232 # it to the right patchset, if it is not yet there.
233
234 # Find revision info for the patchset we want.
235 if patchset is not None:
236 for rev, rev_info in info['revisions'].iteritems():
237 if str(rev_info['_number']) == str(patchset):
238 break
239 else:
240 raise Exception('patchset %s doesn\'t exist in issue %s' % (
241 patchset, issue))
242 else:
243 rev = info['current_revision']
244 rev_info = info['revisions'][rev]
245
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100246 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000247
248 def GetChangeOwner(self, issue):
249 return self.GetChangeInfo(issue)['owner']['email']
250
251 def GetChangeReviewers(self, issue, approving_only=True):
Aaron Gable8b478f02017-07-31 15:33:19 -0700252 changeinfo = self.GetChangeInfo(issue)
253 if approving_only:
254 labelinfo = changeinfo.get('labels', {}).get('Code-Review', {})
255 values = labelinfo.get('values', {}).keys()
256 try:
257 max_value = max(int(v) for v in values)
258 reviewers = [r for r in labelinfo.get('all', [])
259 if r.get('value', 0) == max_value]
260 except ValueError: # values is the empty list
261 reviewers = []
262 else:
263 reviewers = changeinfo.get('reviewers', {}).get('REVIEWER', [])
264 return [r.get('email') for r in reviewers]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000265
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000266
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000267class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000268 """An instance of OutputApi gets passed to presubmit scripts so that they
269 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000270 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000271 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000272 PresubmitError = _PresubmitError
273 PresubmitPromptWarning = _PresubmitPromptWarning
274 PresubmitNotifyResult = _PresubmitNotifyResult
275 MailTextResult = _MailTextResult
276
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000277 def __init__(self, is_committing):
278 self.is_committing = is_committing
279
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000280 def PresubmitPromptOrNotify(self, *args, **kwargs):
281 """Warn the user when uploading, but only notify if committing."""
282 if self.is_committing:
283 return self.PresubmitNotifyResult(*args, **kwargs)
284 return self.PresubmitPromptWarning(*args, **kwargs)
285
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800286 def EnsureCQIncludeTrybotsAreAdded(self, cl, bots_to_include, message):
287 """Helper for any PostUploadHook wishing to add CQ_INCLUDE_TRYBOTS.
288
289 Merges the bots_to_include into the current CQ_INCLUDE_TRYBOTS list,
290 keeping it alphabetically sorted. Returns the results that should be
291 returned from the PostUploadHook.
292
293 Args:
294 cl: The git_cl.Changelist object.
295 bots_to_include: A list of strings of bots to include, in the form
296 "master:slave".
297 message: A message to be printed in the case that
298 CQ_INCLUDE_TRYBOTS was updated.
299 """
300 description = cl.GetDescription(force=True)
Aaron Gableb584c4f2017-04-26 16:28:08 -0700301 include_re = re.compile(r'^CQ_INCLUDE_TRYBOTS=(.*)$', re.M | re.I)
302
303 prior_bots = []
304 if cl.IsGerrit():
305 trybot_footers = git_footers.parse_footers(description).get(
306 git_footers.normalize_name('Cq-Include-Trybots'), [])
307 for f in trybot_footers:
308 prior_bots += [b.strip() for b in f.split(';') if b.strip()]
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800309 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700310 trybot_tags = include_re.finditer(description)
311 for t in trybot_tags:
312 prior_bots += [b.strip() for b in t.group(1).split(';') if b.strip()]
313
314 if set(prior_bots) >= set(bots_to_include):
315 return []
316 all_bots = ';'.join(sorted(set(prior_bots) | set(bots_to_include)))
317
318 if cl.IsGerrit():
319 description = git_footers.remove_footer(
320 description, 'Cq-Include-Trybots')
321 description = git_footers.add_footer(
322 description, 'Cq-Include-Trybots', all_bots,
323 before_keys=['Change-Id'])
324 else:
325 new_include_trybots = 'CQ_INCLUDE_TRYBOTS=%s' % all_bots
326 m = include_re.search(description)
327 if m:
328 description = include_re.sub(new_include_trybots, description)
Kenneth Russelldf6e7342017-04-24 17:07:41 -0700329 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700330 description = '%s\n%s\n' % (description, new_include_trybots)
331
332 cl.UpdateDescription(description, force=True)
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800333 return [self.PresubmitNotifyResult(message)]
334
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000335
336class InputApi(object):
337 """An instance of this object is passed to presubmit scripts so they can
338 know stuff about the change they're looking at.
339 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000340 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800341 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000342
maruel@chromium.org3410d912009-06-09 20:56:16 +0000343 # File extensions that are considered source files from a style guide
344 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000345 #
346 # Files without an extension aren't included in the list. If you want to
347 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
348 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000349 DEFAULT_WHITE_LIST = (
350 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000351 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
352 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000353 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000354 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000355 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000356 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000357 )
358
359 # Path regexp that should be excluded from being considered containing source
360 # files. Don't modify this list from a presubmit script!
361 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000362 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000363 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000364 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
365 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000366 # Output directories (just in case)
367 r".*\bDebug[\\\/].*",
368 r".*\bRelease[\\\/].*",
369 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000370 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000371 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000372 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000373 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000374 r"(|.*[\\\/])\.git[\\\/].*",
375 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000376 # There is no point in processing a patch file.
377 r".+\.diff$",
378 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000379 )
380
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000381 def __init__(self, change, presubmit_path, is_committing,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000382 rietveld_obj, verbose, gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000383 """Builds an InputApi object.
384
385 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000386 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000387 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000388 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000389 rietveld_obj: rietveld.Rietveld client object
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000390 gerrit_obj: provides basic Gerrit codereview functionality.
391 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000392 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000393 # Version number of the presubmit_support script.
394 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000395 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000396 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000397 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000398 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000399 self.dry_run = dry_run
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000400 # TBD
401 self.host_url = 'http://codereview.chromium.org'
402 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000403 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000404
405 # We expose various modules and functions as attributes of the input_api
406 # so that presubmit scripts don't have to import them.
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +0900407 self.ast = ast
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000408 self.basename = os.path.basename
409 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000410 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000411 self.cStringIO = cStringIO
dcheng091b7db2016-06-16 01:27:51 -0700412 self.fnmatch = fnmatch
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000413 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000414 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000415 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000416 self.os_listdir = os.listdir
417 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000418 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000419 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000420 self.pickle = pickle
421 self.marshal = marshal
422 self.re = re
423 self.subprocess = subprocess
424 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000425 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000426 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000427 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000428 self.urllib2 = urllib2
429
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000430 # To easily fork python.
431 self.python_executable = sys.executable
432 self.environ = os.environ
433
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000434 # InputApi.platform is the platform you're currently running on.
435 self.platform = sys.platform
436
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000437 self.cpu_count = multiprocessing.cpu_count()
438
Aaron Gable873c28d2017-09-05 16:19:04 -0700439 # We initialize here because in RunTests, the current working directory has
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000440 # changed, which causes Pool() to explode fantastically when run on windows
441 # (because it tries to load the __main__ module, which imports lots of
442 # things relative to the current working directory).
Aaron Gable873c28d2017-09-05 16:19:04 -0700443 # We capture ctrl-c in the initializer to prevent massive console spew when
444 # cancelling all of the processes with ctrl-c.
445 def _capture_interrupt():
446 CTRL_C = signal.SIGINT
447 if sys.platform == 'win32':
448 CTRL_C = signal.CTRL_C_EVENT
449 signal.signal(CTRL_C, lambda x, y: sys.exit(1))
450 self._run_tests_pool = multiprocessing.Pool(
451 self.cpu_count, _capture_interrupt)
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000452
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000453 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000454 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000455
456 # We carry the canned checks so presubmit scripts can easily use them.
457 self.canned_checks = presubmit_canned_checks
458
Jochen Eisinger72606f82017-04-04 10:44:18 +0200459
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000460 # TODO(dpranke): figure out a list of all approved owners for a repo
461 # in order to be able to handle wildcard OWNERS files?
462 self.owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +0200463 fopen=file, os_path=self.os_path)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200464 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000465 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000466 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000467
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000468 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000469 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000470 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800471 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000472 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000473 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000474 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
475 for (a, b, header) in cpplint._re_pattern_templates
476 ]
477
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000478 def PresubmitLocalPath(self):
479 """Returns the local path of the presubmit script currently being run.
480
481 This is useful if you don't want to hard-code absolute paths in the
482 presubmit script. For example, It can be used to find another file
483 relative to the PRESUBMIT.py script, so the whole tree can be branched and
484 the presubmit script still works, without editing its content.
485 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000486 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000487
agable0b65e732016-11-22 09:25:46 -0800488 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000489 """Same as input_api.change.AffectedFiles() except only lists files
490 (and optionally directories) in the same directory as the current presubmit
491 script, or subdirectories thereof.
492 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000493 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000494 if len(dir_with_slash) == 1:
495 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000496
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000497 return filter(
498 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
agable0b65e732016-11-22 09:25:46 -0800499 self.change.AffectedFiles(include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000500
agable0b65e732016-11-22 09:25:46 -0800501 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000502 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800503 paths = [af.LocalPath() for af in self.AffectedFiles()]
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000504 logging.debug("LocalPaths: %s", paths)
505 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000506
agable0b65e732016-11-22 09:25:46 -0800507 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000508 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800509 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000510
agable0b65e732016-11-22 09:25:46 -0800511 def AffectedTestableFiles(self, include_deletes=None):
512 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000513 in the same directory as the current presubmit script, or subdirectories
514 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000515 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000516 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800517 warn("AffectedTestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000518 " is deprecated and ignored" % str(include_deletes),
519 category=DeprecationWarning,
520 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800521 return filter(lambda x: x.IsTestableFile(),
522 self.AffectedFiles(include_deletes=False))
523
524 def AffectedTextFiles(self, include_deletes=None):
525 """An alias to AffectedTestableFiles for backwards compatibility."""
526 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000527
maruel@chromium.org3410d912009-06-09 20:56:16 +0000528 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
529 """Filters out files that aren't considered "source file".
530
531 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
532 and InputApi.DEFAULT_BLACK_LIST is used respectively.
533
534 The lists will be compiled as regular expression and
535 AffectedFile.LocalPath() needs to pass both list.
536
537 Note: Copy-paste this function to suit your needs or use a lambda function.
538 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000539 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000540 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000541 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000542 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000543 return True
544 return False
545 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
546 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
547
548 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800549 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000550
551 If source_file is None, InputApi.FilterSourceFile() is used.
552 """
553 if not source_file:
554 source_file = self.FilterSourceFile
agable0b65e732016-11-22 09:25:46 -0800555 return filter(source_file, self.AffectedTestableFiles())
maruel@chromium.org3410d912009-06-09 20:56:16 +0000556
557 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000558 """An iterator over all text lines in "new" version of changed files.
559
560 Only lists lines from new or modified text files in the change that are
561 contained by the directory of the currently executing presubmit script.
562
563 This is useful for doing line-by-line regex checks, like checking for
564 trailing whitespace.
565
566 Yields:
567 a 3 tuple:
568 the AffectedFile instance of the current file;
569 integer line number (1-based); and
570 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000571
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000572 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000573 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000574 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000575 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000576
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000577 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000578 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000579
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000580 Deny reading anything outside the repository.
581 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000582 if isinstance(file_item, AffectedFile):
583 file_item = file_item.AbsoluteLocalPath()
584 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000585 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000586 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000587
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000588 @property
589 def tbr(self):
590 """Returns if a change is TBR'ed."""
Jeremy Romandce22502017-06-20 15:37:29 -0400591 return 'TBR' in self.change.tags or self.change.TBRsFromDescription()
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000592
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000593 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000594 tests = []
595 msgs = []
596 for t in tests_mix:
597 if isinstance(t, OutputApi.PresubmitResult):
598 msgs.append(t)
599 else:
600 assert issubclass(t.message, _PresubmitResult)
601 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000602 if self.verbose:
603 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000604 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000605 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000606 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000607 else:
608 msgs.extend(map(CallCommand, tests))
609 return [m for m in msgs if m]
610
scottmg86099d72016-09-01 09:16:51 -0700611 def ShutdownPool(self):
612 self._run_tests_pool.close()
613 self._run_tests_pool.join()
614 self._run_tests_pool = None
615
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000616
nick@chromium.orgff526192013-06-10 19:30:26 +0000617class _DiffCache(object):
618 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000619 def __init__(self, upstream=None):
620 """Stores the upstream revision against which all diffs will be computed."""
621 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000622
623 def GetDiff(self, path, local_root):
624 """Get the diff for a particular path."""
625 raise NotImplementedError()
626
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700627 def GetOldContents(self, path, local_root):
628 """Get the old version for a particular path."""
629 raise NotImplementedError()
630
nick@chromium.orgff526192013-06-10 19:30:26 +0000631
nick@chromium.orgff526192013-06-10 19:30:26 +0000632class _GitDiffCache(_DiffCache):
633 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000634 def __init__(self, upstream):
635 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000636 self._diffs_by_file = None
637
638 def GetDiff(self, path, local_root):
639 if not self._diffs_by_file:
640 # Compute a single diff for all files and parse the output; should
641 # with git this is much faster than computing one diff for each file.
642 diffs = {}
643
644 # Don't specify any filenames below, because there are command line length
645 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000646 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
647 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000648
649 # This regex matches the path twice, separated by a space. Note that
650 # filename itself may contain spaces.
651 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
652 current_diff = []
653 keep_line_endings = True
654 for x in unified_diff.splitlines(keep_line_endings):
655 match = file_marker.match(x)
656 if match:
657 # Marks the start of a new per-file section.
658 diffs[match.group('filename')] = current_diff = [x]
659 elif x.startswith('diff --git'):
660 raise PresubmitFailure('Unexpected diff line: %s' % x)
661 else:
662 current_diff.append(x)
663
664 self._diffs_by_file = dict(
665 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
666
667 if path not in self._diffs_by_file:
668 raise PresubmitFailure(
669 'Unified diff did not contain entry for file %s' % path)
670
671 return self._diffs_by_file[path]
672
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700673 def GetOldContents(self, path, local_root):
674 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
675
nick@chromium.orgff526192013-06-10 19:30:26 +0000676
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000677class AffectedFile(object):
678 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000679
680 DIFF_CACHE = _DiffCache
681
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000682 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800683 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000684 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000685 self._path = path
686 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000687 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000688 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000689 self._cached_changed_contents = None
690 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000691 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700692 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000693
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000694 def LocalPath(self):
695 """Returns the path of this file on the local disk relative to client root.
696 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000697 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000698
699 def AbsoluteLocalPath(self):
700 """Returns the absolute path of this file on the local disk.
701 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000702 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000703
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000704 def Action(self):
705 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000706 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000707
agable0b65e732016-11-22 09:25:46 -0800708 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000709 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000710
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000711 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000712 raise NotImplementedError() # Implement when needed
713
agable0b65e732016-11-22 09:25:46 -0800714 def IsTextFile(self):
715 """An alias to IsTestableFile for backwards compatibility."""
716 return self.IsTestableFile()
717
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700718 def OldContents(self):
719 """Returns an iterator over the lines in the old version of file.
720
Daniel Cheng2da34fe2017-03-21 20:42:12 -0700721 The old version is the file before any modifications in the user's
722 workspace, i.e. the "left hand side".
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700723
724 Contents will be empty if the file is a directory or does not exist.
725 Note: The carriage returns (LF or CR) are stripped off.
726 """
727 return self._diff_cache.GetOldContents(self.LocalPath(),
728 self._local_root).splitlines()
729
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000730 def NewContents(self):
731 """Returns an iterator over the lines in the new version of file.
732
733 The new version is the file in the user's workspace, i.e. the "right hand
734 side".
735
736 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000737 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000738 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000739 if self._cached_new_contents is None:
740 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800741 try:
742 self._cached_new_contents = gclient_utils.FileRead(
743 self.AbsoluteLocalPath(), 'rU').splitlines()
744 except IOError:
745 pass # File not found? That's fine; maybe it was deleted.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000746 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000747
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000748 def ChangedContents(self):
749 """Returns a list of tuples (line number, line text) of all new lines.
750
751 This relies on the scm diff output describing each changed code section
752 with a line of the form
753
754 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
755 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000756 if self._cached_changed_contents is not None:
757 return self._cached_changed_contents[:]
758 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000759 line_num = 0
760
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000761 for line in self.GenerateScmDiff().splitlines():
762 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
763 if m:
764 line_num = int(m.groups(1)[0])
765 continue
766 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000767 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000768 if not line.startswith('-'):
769 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000770 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000771
maruel@chromium.org5de13972009-06-10 18:16:06 +0000772 def __str__(self):
773 return self.LocalPath()
774
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000775 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000776 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000777
maruel@chromium.org58407af2011-04-12 23:15:57 +0000778
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000779class GitAffectedFile(AffectedFile):
780 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000781 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800782 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000783
nick@chromium.orgff526192013-06-10 19:30:26 +0000784 DIFF_CACHE = _GitDiffCache
785
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000786 def __init__(self, *args, **kwargs):
787 AffectedFile.__init__(self, *args, **kwargs)
788 self._server_path = None
agable0b65e732016-11-22 09:25:46 -0800789 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000790
agable0b65e732016-11-22 09:25:46 -0800791 def IsTestableFile(self):
792 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000793 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -0800794 # A deleted file is not testable.
795 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000796 else:
agable0b65e732016-11-22 09:25:46 -0800797 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
798 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000799
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000800
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000801class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000802 """Describe a change.
803
804 Used directly by the presubmit scripts to query the current change being
805 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000806
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000807 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000808 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000809 self.KEY: equivalent to tags['KEY']
810 """
811
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000812 _AFFECTED_FILES = AffectedFile
813
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000814 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000815 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000816 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000817 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000818
maruel@chromium.org58407af2011-04-12 23:15:57 +0000819 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000820 self, name, description, local_root, files, issue, patchset, author,
821 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000822 if files is None:
823 files = []
824 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000825 # Convert root into an absolute path.
826 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000827 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000828 self.issue = issue
829 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000830 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000831
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000832 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000833 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000834 self._description_without_tags = ''
835 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000836
maruel@chromium.orge085d812011-10-10 19:49:15 +0000837 assert all(
838 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
839
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000840 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000841 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000842 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
843 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000844 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000845
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000846 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000847 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000848 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000849
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000850 def DescriptionText(self):
851 """Returns the user-entered changelist description, minus tags.
852
853 Any line in the user-provided description starting with e.g. "FOO="
854 (whitespace permitted before and around) is considered a tag line. Such
855 lines are stripped out of the description this function returns.
856 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000857 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000858
859 def FullDescriptionText(self):
860 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000861 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000862
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000863 def SetDescriptionText(self, description):
864 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000865
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000866 Also updates the list of tags."""
867 self._full_description = description
868
869 # From the description text, build up a dictionary of key/value pairs
870 # plus the description minus all key/value or "tag" lines.
871 description_without_tags = []
872 self.tags = {}
873 for line in self._full_description.splitlines():
874 m = self.TAG_LINE_RE.match(line)
875 if m:
876 self.tags[m.group('key')] = m.group('value')
877 else:
878 description_without_tags.append(line)
879
880 # Change back to text and remove whitespace at end.
881 self._description_without_tags = (
882 '\n'.join(description_without_tags).rstrip())
883
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000884 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000885 """Returns the repository (checkout) root directory for this change,
886 as an absolute path.
887 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000888 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000889
890 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000891 """Return tags directly as attributes on the object."""
892 if not re.match(r"^[A-Z_]*$", attr):
893 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000894 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000895
Aaron Gablefc03e672017-05-15 14:09:42 -0700896 def BugsFromDescription(self):
897 """Returns all bugs referenced in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700898 tags = [b.strip() for b in self.tags.get('BUG', '').split(',') if b.strip()]
899 footers = git_footers.parse_footers(self._full_description).get('Bug', [])
900 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -0700901
902 def ReviewersFromDescription(self):
903 """Returns all reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700904 # We don't support a "R:" git-footer for reviewers; that is in metadata.
905 tags = [r.strip() for r in self.tags.get('R', '').split(',') if r.strip()]
906 return sorted(set(tags))
Aaron Gablefc03e672017-05-15 14:09:42 -0700907
908 def TBRsFromDescription(self):
909 """Returns all TBR reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700910 tags = [r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()]
911 # TODO(agable): Remove support for 'Tbr:' when TBRs are programmatically
912 # determined by self-CR+1s.
913 footers = git_footers.parse_footers(self._full_description).get('Tbr', [])
914 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -0700915
916 # TODO(agable): Delete these once we're sure they're unused.
917 @property
918 def BUG(self):
919 return ','.join(self.BugsFromDescription())
920 @property
921 def R(self):
922 return ','.join(self.ReviewersFromDescription())
923 @property
924 def TBR(self):
925 return ','.join(self.TBRsFromDescription())
926
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000927 def AllFiles(self, root=None):
928 """List all files under source control in the repo."""
929 raise NotImplementedError()
930
agable0b65e732016-11-22 09:25:46 -0800931 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000932 """Returns a list of AffectedFile instances for all files in the change.
933
934 Args:
935 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +0000936 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000937
938 Returns:
939 [AffectedFile(path, action), AffectedFile(path, action)]
940 """
agable0b65e732016-11-22 09:25:46 -0800941 affected = filter(file_filter, self._affected_files)
sail@chromium.org5538e022011-05-12 17:53:16 +0000942
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000943 if include_deletes:
944 return affected
Lei Zhang9611c4c2017-04-04 01:41:56 -0700945 return filter(lambda x: x.Action() != 'D', affected)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000946
agable0b65e732016-11-22 09:25:46 -0800947 def AffectedTestableFiles(self, include_deletes=None):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000948 """Return a list of the existing text files in a change."""
949 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800950 warn("AffectedTeestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000951 " is deprecated and ignored" % str(include_deletes),
952 category=DeprecationWarning,
953 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800954 return filter(lambda x: x.IsTestableFile(),
955 self.AffectedFiles(include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000956
agable0b65e732016-11-22 09:25:46 -0800957 def AffectedTextFiles(self, include_deletes=None):
958 """An alias to AffectedTestableFiles for backwards compatibility."""
959 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000960
agable0b65e732016-11-22 09:25:46 -0800961 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000962 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800963 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000964
agable0b65e732016-11-22 09:25:46 -0800965 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000966 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800967 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000968
969 def RightHandSideLines(self):
970 """An iterator over all text lines in "new" version of changed files.
971
972 Lists lines from new or modified text files in the change.
973
974 This is useful for doing line-by-line regex checks, like checking for
975 trailing whitespace.
976
977 Yields:
978 a 3 tuple:
979 the AffectedFile instance of the current file;
980 integer line number (1-based); and
981 the contents of the line as a string.
982 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000983 return _RightHandSideLinesImpl(
984 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -0800985 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000986
Jochen Eisingerd0573ec2017-04-13 10:55:06 +0200987 def OriginalOwnersFiles(self):
988 """A map from path names of affected OWNERS files to their old content."""
989 def owners_file_filter(f):
990 return 'OWNERS' in os.path.split(f.LocalPath())[1]
991 files = self.AffectedFiles(file_filter=owners_file_filter)
992 return dict([(f.LocalPath(), f.OldContents()) for f in files])
993
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000994
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000995class GitChange(Change):
996 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000997 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000998
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000999 def AllFiles(self, root=None):
1000 """List all files under source control in the repo."""
1001 root = root or self.RepositoryRoot()
1002 return subprocess.check_output(
1003 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
1004
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001005
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001006def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001007 """Finds all presubmit files that apply to a given set of source files.
1008
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001009 If inherit-review-settings-ok is present right under root, looks for
1010 PRESUBMIT.py in directories enclosing root.
1011
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001012 Args:
1013 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001014 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001015
1016 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001017 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001018 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001019 files = [normpath(os.path.join(root, f)) for f in files]
1020
1021 # List all the individual directories containing files.
1022 directories = set([os.path.dirname(f) for f in files])
1023
1024 # Ignore root if inherit-review-settings-ok is present.
1025 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1026 root = None
1027
1028 # Collect all unique directories that may contain PRESUBMIT.py.
1029 candidates = set()
1030 for directory in directories:
1031 while True:
1032 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001033 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001034 candidates.add(directory)
1035 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001036 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001037 parent_dir = os.path.dirname(directory)
1038 if parent_dir == directory:
1039 # We hit the system root directory.
1040 break
1041 directory = parent_dir
1042
1043 # Look for PRESUBMIT.py in all candidate directories.
1044 results = []
1045 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -07001046 try:
1047 for f in os.listdir(directory):
1048 p = os.path.join(directory, f)
1049 if os.path.isfile(p) and re.match(
1050 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1051 results.append(p)
1052 except OSError:
1053 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001054
tobiasjs2836bcf2016-08-16 04:08:16 -07001055 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001056 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001057
1058
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001059class GetTryMastersExecuter(object):
1060 @staticmethod
1061 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1062 """Executes GetPreferredTryMasters() from a single presubmit script.
1063
1064 Args:
1065 script_text: The text of the presubmit script.
1066 presubmit_path: Project script to run.
1067 project: Project name to pass to presubmit script for bot selection.
1068
1069 Return:
1070 A map of try masters to map of builders to set of tests.
1071 """
1072 context = {}
1073 try:
1074 exec script_text in context
1075 except Exception, e:
1076 raise PresubmitFailure('"%s" had an exception.\n%s'
1077 % (presubmit_path, e))
1078
1079 function_name = 'GetPreferredTryMasters'
1080 if function_name not in context:
1081 return {}
1082 get_preferred_try_masters = context[function_name]
1083 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1084 raise PresubmitFailure(
1085 'Expected function "GetPreferredTryMasters" to take two arguments.')
1086 return get_preferred_try_masters(project, change)
1087
1088
rmistry@google.com5626a922015-02-26 14:03:30 +00001089class GetPostUploadExecuter(object):
1090 @staticmethod
1091 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1092 """Executes PostUploadHook() from a single presubmit script.
1093
1094 Args:
1095 script_text: The text of the presubmit script.
1096 presubmit_path: Project script to run.
1097 cl: The Changelist object.
1098 change: The Change object.
1099
1100 Return:
1101 A list of results objects.
1102 """
1103 context = {}
1104 try:
1105 exec script_text in context
1106 except Exception, e:
1107 raise PresubmitFailure('"%s" had an exception.\n%s'
1108 % (presubmit_path, e))
1109
1110 function_name = 'PostUploadHook'
1111 if function_name not in context:
1112 return {}
1113 post_upload_hook = context[function_name]
1114 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1115 raise PresubmitFailure(
1116 'Expected function "PostUploadHook" to take three arguments.')
1117 return post_upload_hook(cl, change, OutputApi(False))
1118
1119
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001120def _MergeMasters(masters1, masters2):
1121 """Merges two master maps. Merges also the tests of each builder."""
1122 result = {}
1123 for (master, builders) in itertools.chain(masters1.iteritems(),
1124 masters2.iteritems()):
1125 new_builders = result.setdefault(master, {})
1126 for (builder, tests) in builders.iteritems():
1127 new_builders.setdefault(builder, set([])).update(tests)
1128 return result
1129
1130
1131def DoGetTryMasters(change,
1132 changed_files,
1133 repository_root,
1134 default_presubmit,
1135 project,
1136 verbose,
1137 output_stream):
1138 """Get the list of try masters from the presubmit scripts.
1139
1140 Args:
1141 changed_files: List of modified files.
1142 repository_root: The repository root.
1143 default_presubmit: A default presubmit script to execute in any case.
1144 project: Optional name of a project used in selecting trybots.
1145 verbose: Prints debug info.
1146 output_stream: A stream to write debug output to.
1147
1148 Return:
1149 Map of try masters to map of builders to set of tests.
1150 """
1151 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1152 if not presubmit_files and verbose:
1153 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1154 results = {}
1155 executer = GetTryMastersExecuter()
1156
1157 if default_presubmit:
1158 if verbose:
1159 output_stream.write("Running default presubmit script.\n")
1160 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1161 results = _MergeMasters(results, executer.ExecPresubmitScript(
1162 default_presubmit, fake_path, project, change))
1163 for filename in presubmit_files:
1164 filename = os.path.abspath(filename)
1165 if verbose:
1166 output_stream.write("Running %s\n" % filename)
1167 # Accept CRLF presubmit script.
1168 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1169 results = _MergeMasters(results, executer.ExecPresubmitScript(
1170 presubmit_script, filename, project, change))
1171
1172 # Make sets to lists again for later JSON serialization.
1173 for builders in results.itervalues():
1174 for builder in builders:
1175 builders[builder] = list(builders[builder])
1176
1177 if results and verbose:
1178 output_stream.write('%s\n' % str(results))
1179 return results
1180
1181
rmistry@google.com5626a922015-02-26 14:03:30 +00001182def DoPostUploadExecuter(change,
1183 cl,
1184 repository_root,
1185 verbose,
1186 output_stream):
1187 """Execute the post upload hook.
1188
1189 Args:
1190 change: The Change object.
1191 cl: The Changelist object.
1192 repository_root: The repository root.
1193 verbose: Prints debug info.
1194 output_stream: A stream to write debug output to.
1195 """
1196 presubmit_files = ListRelevantPresubmitFiles(
1197 change.LocalPaths(), repository_root)
1198 if not presubmit_files and verbose:
1199 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1200 results = []
1201 executer = GetPostUploadExecuter()
1202 # The root presubmit file should be executed after the ones in subdirectories.
1203 # i.e. the specific post upload hooks should run before the general ones.
1204 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1205 presubmit_files.reverse()
1206
1207 for filename in presubmit_files:
1208 filename = os.path.abspath(filename)
1209 if verbose:
1210 output_stream.write("Running %s\n" % filename)
1211 # Accept CRLF presubmit script.
1212 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1213 results.extend(executer.ExecPresubmitScript(
1214 presubmit_script, filename, cl, change))
1215 output_stream.write('\n')
1216 if results:
1217 output_stream.write('** Post Upload Hook Messages **\n')
1218 for result in results:
1219 result.handle(output_stream)
1220 output_stream.write('\n')
1221
1222 return results
1223
1224
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001225class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001226 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001227 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001228 """
1229 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001230 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001231 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001232 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001233 gerrit_obj: provides basic Gerrit codereview functionality.
1234 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001235 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001236 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001237 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001238 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001239 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001240 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001241 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001242
1243 def ExecPresubmitScript(self, script_text, presubmit_path):
1244 """Executes a single presubmit script.
1245
1246 Args:
1247 script_text: The text of the presubmit script.
1248 presubmit_path: The path to the presubmit file (this will be reported via
1249 input_api.PresubmitLocalPath()).
1250
1251 Return:
1252 A list of result objects, empty if no problems.
1253 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001254
chase@chromium.org8e416c82009-10-06 04:30:44 +00001255 # Change to the presubmit file's directory to support local imports.
1256 main_path = os.getcwd()
1257 os.chdir(os.path.dirname(presubmit_path))
1258
1259 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001260 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001261 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001262 gerrit_obj=self.gerrit, dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001263 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001264 try:
1265 exec script_text in context
1266 except Exception, e:
1267 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001268
1269 # These function names must change if we make substantial changes to
1270 # the presubmit API that are not backwards compatible.
1271 if self.committing:
1272 function_name = 'CheckChangeOnCommit'
1273 else:
1274 function_name = 'CheckChangeOnUpload'
1275 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001276 context['__args'] = (input_api, OutputApi(self.committing))
tobiasjs2836bcf2016-08-16 04:08:16 -07001277 logging.debug('Running %s in %s', function_name, presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001278 result = eval(function_name + '(*__args)', context)
tobiasjs2836bcf2016-08-16 04:08:16 -07001279 logging.debug('Running %s done.', function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001280 if not (isinstance(result, types.TupleType) or
1281 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001282 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001283 'Presubmit functions must return a tuple or list')
1284 for item in result:
1285 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001286 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001287 'All presubmit results must be of types derived from '
1288 'output_api.PresubmitResult')
1289 else:
1290 result = () # no error since the script doesn't care about current event.
1291
scottmg86099d72016-09-01 09:16:51 -07001292 input_api.ShutdownPool()
1293
chase@chromium.org8e416c82009-10-06 04:30:44 +00001294 # Return the process to the original working directory.
1295 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001296 return result
1297
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001298def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001299 committing,
1300 verbose,
1301 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001302 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001303 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001304 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001305 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001306 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001307 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001308 """Runs all presubmit checks that apply to the files in the change.
1309
1310 This finds all PRESUBMIT.py files in directories enclosing the files in the
1311 change (up to the repository root) and calls the relevant entrypoint function
1312 depending on whether the change is being committed or uploaded.
1313
1314 Prints errors, warnings and notifications. Prompts the user for warnings
1315 when needed.
1316
1317 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001318 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001319 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001320 verbose: Prints debug info.
1321 output_stream: A stream to write output from presubmit tests to.
1322 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001323 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001324 may_prompt: Enable (y/n) questions on warning or error. If False,
1325 any questions are answered with yes by default.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001326 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001327 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001328 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001329
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001330 Warning:
1331 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1332 SHOULD be sys.stdin.
1333
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001334 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001335 A PresubmitOutput object. Use output.should_continue() to figure out
1336 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001337 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001338 old_environ = os.environ
1339 try:
1340 # Make sure python subprocesses won't generate .pyc files.
1341 os.environ = os.environ.copy()
1342 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001343
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001344 output = PresubmitOutput(input_stream, output_stream)
1345 if committing:
1346 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001347 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001348 output.write("Running presubmit upload checks ...\n")
1349 start_time = time.time()
1350 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001351 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001352 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001353 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001354 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001355 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001356 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001357 if default_presubmit:
1358 if verbose:
1359 output.write("Running default presubmit script.\n")
1360 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1361 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1362 for filename in presubmit_files:
1363 filename = os.path.abspath(filename)
1364 if verbose:
1365 output.write("Running %s\n" % filename)
1366 # Accept CRLF presubmit script.
1367 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1368 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001369
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001370 errors = []
1371 notifications = []
1372 warnings = []
1373 for result in results:
1374 if result.fatal:
1375 errors.append(result)
1376 elif result.should_prompt:
1377 warnings.append(result)
1378 else:
1379 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001380
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001381 output.write('\n')
1382 for name, items in (('Messages', notifications),
1383 ('Warnings', warnings),
1384 ('ERRORS', errors)):
1385 if items:
1386 output.write('** Presubmit %s **\n' % name)
1387 for item in items:
1388 item.handle(output)
1389 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001390
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001391 total_time = time.time() - start_time
1392 if total_time > 1.0:
1393 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001394
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001395 if errors:
1396 output.fail()
1397 elif warnings:
1398 output.write('There were presubmit warnings. ')
1399 if may_prompt:
1400 output.prompt_yes_no('Are you sure you wish to continue? (y/N): ')
1401 else:
1402 output.write('Presubmit checks passed.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001403
1404 global _ASKED_FOR_FEEDBACK
1405 # Ask for feedback one time out of 5.
1406 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001407 output.write(
1408 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1409 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1410 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001411 _ASKED_FOR_FEEDBACK = True
1412 return output
1413 finally:
1414 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001415
1416
1417def ScanSubDirs(mask, recursive):
1418 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001419 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001420
1421 results = []
1422 for root, dirs, files in os.walk('.'):
1423 if '.svn' in dirs:
1424 dirs.remove('.svn')
1425 if '.git' in dirs:
1426 dirs.remove('.git')
1427 for name in files:
1428 if fnmatch.fnmatch(name, mask):
1429 results.append(os.path.join(root, name))
1430 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001431
1432
1433def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001434 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001435 files = []
1436 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001437 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001438 return files
1439
1440
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001441def load_files(options, args):
1442 """Tries to determine the SCM."""
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001443 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001444 if args:
1445 files = ParseFiles(args, options.recursive)
agable0b65e732016-11-22 09:25:46 -08001446 change_scm = scm.determine_scm(options.root)
1447 if change_scm == 'git':
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001448 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001449 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001450 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001451 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001452 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001453 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001454 if not files:
1455 return None, None
1456 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001457 return change_class, files
1458
1459
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001460class NonexistantCannedCheckFilter(Exception):
1461 pass
1462
1463
1464@contextlib.contextmanager
1465def canned_check_filter(method_names):
1466 filtered = {}
1467 try:
1468 for method_name in method_names:
1469 if not hasattr(presubmit_canned_checks, method_name):
1470 raise NonexistantCannedCheckFilter(method_name)
1471 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1472 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1473 yield
1474 finally:
1475 for name, method in filtered.iteritems():
1476 setattr(presubmit_canned_checks, name, method)
1477
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001478
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001479def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001480 """Runs an external program, potentially from a child process created by the
1481 multiprocessing module.
1482
1483 multiprocessing needs a top level function with a single argument.
1484 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001485 cmd_data.kwargs['stdout'] = subprocess.PIPE
1486 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1487 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001488 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001489 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001490 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001491 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001492 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001493 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001494 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1495 if code != 0:
1496 return cmd_data.message(
1497 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1498 if cmd_data.info:
1499 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001500
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001501
sbc@chromium.org013731e2015-02-26 18:28:43 +00001502def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001503 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001504 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001505 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001506 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001507 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1508 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001509 parser.add_option("-r", "--recursive", action="store_true",
1510 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001511 parser.add_option("-v", "--verbose", action="count", default=0,
1512 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001513 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001514 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001515 parser.add_option("--description", default='')
1516 parser.add_option("--issue", type='int', default=0)
1517 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001518 parser.add_option("--root", default=os.getcwd(),
1519 help="Search for PRESUBMIT.py up to this directory. "
1520 "If inherit-review-settings-ok is present in this "
1521 "directory, parent directories up to the root file "
1522 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001523 parser.add_option("--upstream",
1524 help="Git only: the base ref or upstream branch against "
1525 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001526 parser.add_option("--default_presubmit")
1527 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001528 parser.add_option("--skip_canned", action='append', default=[],
1529 help="A list of checks to skip which appear in "
1530 "presubmit_canned_checks. Can be provided multiple times "
1531 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001532 parser.add_option("--dry_run", action='store_true',
1533 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001534 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001535 parser.add_option("--gerrit_fetch", action='store_true',
1536 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001537 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1538 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001539 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1540 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001541 # These are for OAuth2 authentication for bots. See also apply_issue.py
1542 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1543 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1544
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001545 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001546 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001547 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001548
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001549 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001550 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001551 elif options.verbose:
1552 logging.basicConfig(level=logging.INFO)
1553 else:
1554 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001555
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001556 if (any((options.rietveld_url, options.rietveld_email_file,
1557 options.rietveld_fetch, options.rietveld_private_key_file))
1558 and any((options.gerrit_url, options.gerrit_fetch))):
1559 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1560 'allowed')
1561
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001562 if options.rietveld_email and options.rietveld_email_file:
1563 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1564 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001565 if options.rietveld_email_file:
1566 with open(options.rietveld_email_file, "rb") as f:
1567 options.rietveld_email = f.read().strip()
1568
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001569 change_class, files = load_files(options, args)
1570 if not change_class:
1571 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001572 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001573
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001574 rietveld_obj, gerrit_obj = None, None
1575
maruel@chromium.org239f4112011-06-03 20:08:23 +00001576 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001577 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001578 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001579 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1580 options.rietveld_url,
1581 options.rietveld_email,
1582 options.rietveld_private_key_file)
1583 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001584 rietveld_obj = rietveld.CachingRietveld(
1585 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001586 auth_config,
1587 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001588 if options.rietveld_fetch:
1589 assert options.issue
1590 props = rietveld_obj.get_issue_properties(options.issue, False)
1591 options.author = props['owner_email']
1592 options.description = props['description']
1593 logging.info('Got author: "%s"', options.author)
1594 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001595
1596 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001597 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001598 rietveld_obj = None
1599 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1600 options.author = gerrit_obj.GetChangeOwner(options.issue)
1601 options.description = gerrit_obj.GetChangeDescription(options.issue,
1602 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001603 logging.info('Got author: "%s"', options.author)
1604 logging.info('Got description: """\n%s\n"""', options.description)
1605
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001606 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001607 with canned_check_filter(options.skip_canned):
1608 results = DoPresubmitChecks(
1609 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001610 options.description,
1611 options.root,
1612 files,
1613 options.issue,
1614 options.patchset,
1615 options.author,
1616 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001617 options.commit,
1618 options.verbose,
1619 sys.stdout,
1620 sys.stdin,
1621 options.default_presubmit,
1622 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001623 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001624 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001625 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001626 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001627 except NonexistantCannedCheckFilter, e:
1628 print >> sys.stderr, (
1629 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1630 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001631 except PresubmitFailure, e:
1632 print >> sys.stderr, e
1633 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1634 print >> sys.stderr, 'If all fails, contact maruel@'
1635 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001636
1637
1638if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001639 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001640 try:
1641 sys.exit(main())
1642 except KeyboardInterrupt:
1643 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001644 sys.exit(2)