blob: d560cec8e4da901d00833e134a6a6aca23c203d3 [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.
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +000016import contextlib
Yoshisato Yanagisawa406de132018-06-29 05:43:25 +000017import cPickle # Exposed through the API.
18import cpplint
19import cStringIO # Exposed through the API.
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.
Edward Lesmes8e282792018-04-03 18:50:29 -040033import signal
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034import sys # Parts exposed through API.
35import tempfile # Exposed through the API.
Edward Lesmes8e282792018-04-03 18:50:29 -040036import threading
jam@chromium.org2a891dc2009-08-20 20:33:37 +000037import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000038import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000039import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000040import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000041import urllib2 # Exposed through the API.
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000042import urlparse
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000043from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000044
45# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000046import fix_encoding
Yoshisato Yanagisawa04600b42019-03-15 03:03:41 +000047import gclient_paths # Exposed through the API
48import gclient_utils
Aaron Gableb584c4f2017-04-26 16:28:08 -070049import git_footers
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000050import gerrit_util
dpranke@chromium.org2a009622011-03-01 02:43:31 +000051import owners
Jochen Eisinger76f5fc62017-04-07 16:27:46 +020052import owners_finder
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053import presubmit_canned_checks
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
Edward Lesmes8e282792018-04-03 18:50:29 -040070 self.stdin = kwargs.get('stdin', None)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000071 self.kwargs = kwargs
Edward Lesmes8e282792018-04-03 18:50:29 -040072 self.kwargs['stdout'] = subprocess.PIPE
73 self.kwargs['stderr'] = subprocess.STDOUT
74 self.kwargs['stdin'] = subprocess.PIPE
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000075 self.message = message
76 self.info = None
77
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000078
Edward Lesmes8e282792018-04-03 18:50:29 -040079# Adapted from
80# https://github.com/google/gtest-parallel/blob/master/gtest_parallel.py#L37
81#
82# An object that catches SIGINT sent to the Python process and notices
83# if processes passed to wait() die by SIGINT (we need to look for
84# both of those cases, because pressing Ctrl+C can result in either
85# the main process or one of the subprocesses getting the signal).
86#
87# Before a SIGINT is seen, wait(p) will simply call p.wait() and
88# return the result. Once a SIGINT has been seen (in the main process
89# or a subprocess, including the one the current call is waiting for),
90# wait(p) will call p.terminate() and raise ProcessWasInterrupted.
91class SigintHandler(object):
92 class ProcessWasInterrupted(Exception):
93 pass
94
95 sigint_returncodes = {-signal.SIGINT, # Unix
96 -1073741510, # Windows
97 }
98 def __init__(self):
99 self.__lock = threading.Lock()
100 self.__processes = set()
101 self.__got_sigint = False
102 signal.signal(signal.SIGINT, lambda signal_num, frame: self.interrupt())
103
104 def __on_sigint(self):
105 self.__got_sigint = True
106 while self.__processes:
107 try:
108 self.__processes.pop().terminate()
109 except OSError:
110 pass
111
112 def interrupt(self):
113 with self.__lock:
114 self.__on_sigint()
115
116 def got_sigint(self):
117 with self.__lock:
118 return self.__got_sigint
119
120 def wait(self, p, stdin):
121 with self.__lock:
122 if self.__got_sigint:
123 p.terminate()
124 self.__processes.add(p)
125 stdout, stderr = p.communicate(stdin)
126 code = p.returncode
127 with self.__lock:
128 self.__processes.discard(p)
129 if code in self.sigint_returncodes:
130 self.__on_sigint()
131 if self.__got_sigint:
132 raise self.ProcessWasInterrupted
133 return stdout, stderr
134
135sigint_handler = SigintHandler()
136
137
138class ThreadPool(object):
139 def __init__(self, pool_size=None):
140 self._pool_size = pool_size or multiprocessing.cpu_count()
141 self._messages = []
142 self._messages_lock = threading.Lock()
143 self._tests = []
144 self._tests_lock = threading.Lock()
145 self._nonparallel_tests = []
146
147 def CallCommand(self, test):
148 """Runs an external program.
149
150 This function converts invocation of .py files and invocations of "python"
151 to vpython invocations.
152 """
153 vpython = 'vpython.bat' if sys.platform == 'win32' else 'vpython'
154
155 cmd = test.cmd
156 if cmd[0] == 'python':
157 cmd = list(cmd)
158 cmd[0] = vpython
159 elif cmd[0].endswith('.py'):
160 cmd = [vpython] + cmd
161
162 try:
163 start = time.time()
164 p = subprocess.Popen(cmd, **test.kwargs)
165 stdout, _ = sigint_handler.wait(p, test.stdin)
166 duration = time.time() - start
167 except OSError as e:
168 duration = time.time() - start
169 return test.message(
170 '%s exec failure (%4.2fs)\n %s' % (test.name, duration, e))
171 if p.returncode != 0:
172 return test.message(
173 '%s (%4.2fs) failed\n%s' % (test.name, duration, stdout))
174 if test.info:
175 return test.info('%s (%4.2fs)' % (test.name, duration))
176
177 def AddTests(self, tests, parallel=True):
178 if parallel:
179 self._tests.extend(tests)
180 else:
181 self._nonparallel_tests.extend(tests)
182
183 def RunAsync(self):
184 self._messages = []
185
186 def _WorkerFn():
187 while True:
188 test = None
189 with self._tests_lock:
190 if not self._tests:
191 break
192 test = self._tests.pop()
193 result = self.CallCommand(test)
194 if result:
195 with self._messages_lock:
196 self._messages.append(result)
197
198 def _StartDaemon():
199 t = threading.Thread(target=_WorkerFn)
200 t.daemon = True
201 t.start()
202 return t
203
204 while self._nonparallel_tests:
205 test = self._nonparallel_tests.pop()
206 result = self.CallCommand(test)
207 if result:
208 self._messages.append(result)
209
210 if self._tests:
211 threads = [_StartDaemon() for _ in range(self._pool_size)]
212 for worker in threads:
213 worker.join()
214
215 return self._messages
216
217
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000218def normpath(path):
219 '''Version of os.path.normpath that also changes backward slashes to
220 forward slashes when not running on Windows.
221 '''
222 # This is safe to always do because the Windows version of os.path.normpath
223 # will replace forward slashes with backward slashes.
224 path = path.replace(os.sep, '/')
225 return os.path.normpath(path)
226
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000227
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000228def _RightHandSideLinesImpl(affected_files):
229 """Implements RightHandSideLines for InputApi and GclChange."""
230 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000231 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000232 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000233 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000234
235
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000236class PresubmitOutput(object):
237 def __init__(self, input_stream=None, output_stream=None):
238 self.input_stream = input_stream
239 self.output_stream = output_stream
240 self.reviewers = []
Daniel Cheng7227d212017-11-17 08:12:37 -0800241 self.more_cc = []
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000242 self.written_output = []
243 self.error_count = 0
244
245 def prompt_yes_no(self, prompt_string):
246 self.write(prompt_string)
247 if self.input_stream:
248 response = self.input_stream.readline().strip().lower()
249 if response not in ('y', 'yes'):
250 self.fail()
251 else:
252 self.fail()
253
254 def fail(self):
255 self.error_count += 1
256
257 def should_continue(self):
258 return not self.error_count
259
260 def write(self, s):
261 self.written_output.append(s)
262 if self.output_stream:
263 self.output_stream.write(s)
264
265 def getvalue(self):
266 return ''.join(self.written_output)
267
268
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000269# Top level object so multiprocessing can pickle
270# Public access through OutputApi object.
271class _PresubmitResult(object):
272 """Base class for result objects."""
273 fatal = False
274 should_prompt = False
275
276 def __init__(self, message, items=None, long_text=''):
277 """
278 message: A short one-line message to indicate errors.
279 items: A list of short strings to indicate where errors occurred.
280 long_text: multi-line text output, e.g. from another tool
281 """
282 self._message = message
283 self._items = items or []
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000284 self._long_text = long_text.rstrip()
285
286 def handle(self, output):
287 output.write(self._message)
288 output.write('\n')
289 for index, item in enumerate(self._items):
290 output.write(' ')
291 # Write separately in case it's unicode.
292 output.write(str(item))
293 if index < len(self._items) - 1:
294 output.write(' \\')
295 output.write('\n')
296 if self._long_text:
297 output.write('\n***************\n')
298 # Write separately in case it's unicode.
299 output.write(self._long_text)
300 output.write('\n***************\n')
301 if self.fatal:
302 output.fail()
303
304
305# Top level object so multiprocessing can pickle
306# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000307class _PresubmitError(_PresubmitResult):
308 """A hard presubmit error."""
309 fatal = True
310
311
312# Top level object so multiprocessing can pickle
313# Public access through OutputApi object.
314class _PresubmitPromptWarning(_PresubmitResult):
315 """An warning that prompts the user if they want to continue."""
316 should_prompt = True
317
318
319# Top level object so multiprocessing can pickle
320# Public access through OutputApi object.
321class _PresubmitNotifyResult(_PresubmitResult):
322 """Just print something to the screen -- but it's not even a warning."""
323 pass
324
325
326# Top level object so multiprocessing can pickle
327# Public access through OutputApi object.
328class _MailTextResult(_PresubmitResult):
329 """A warning that should be included in the review request email."""
330 def __init__(self, *args, **kwargs):
331 super(_MailTextResult, self).__init__()
332 raise NotImplementedError()
333
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000334class GerritAccessor(object):
335 """Limited Gerrit functionality for canned presubmit checks to work.
336
337 To avoid excessive Gerrit calls, caches the results.
338 """
339
340 def __init__(self, host):
341 self.host = host
342 self.cache = {}
343
344 def _FetchChangeDetail(self, issue):
345 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100346 try:
347 return gerrit_util.GetChangeDetail(
348 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700349 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100350 except gerrit_util.GerritError as e:
351 if e.http_status == 404:
352 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
353 'no credentials to fetch issue details' % issue)
354 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000355
356 def GetChangeInfo(self, issue):
357 """Returns labels and all revisions (patchsets) for this issue.
358
359 The result is a dictionary according to Gerrit REST Api.
360 https://gerrit-review.googlesource.com/Documentation/rest-api.html
361
362 However, API isn't very clear what's inside, so see tests for example.
363 """
364 assert issue
365 cache_key = int(issue)
366 if cache_key not in self.cache:
367 self.cache[cache_key] = self._FetchChangeDetail(issue)
368 return self.cache[cache_key]
369
370 def GetChangeDescription(self, issue, patchset=None):
371 """If patchset is none, fetches current patchset."""
372 info = self.GetChangeInfo(issue)
373 # info is a reference to cache. We'll modify it here adding description to
374 # it to the right patchset, if it is not yet there.
375
376 # Find revision info for the patchset we want.
377 if patchset is not None:
378 for rev, rev_info in info['revisions'].iteritems():
379 if str(rev_info['_number']) == str(patchset):
380 break
381 else:
382 raise Exception('patchset %s doesn\'t exist in issue %s' % (
383 patchset, issue))
384 else:
385 rev = info['current_revision']
386 rev_info = info['revisions'][rev]
387
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100388 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000389
Mun Yong Jang603d01e2017-12-19 16:38:30 -0800390 def GetDestRef(self, issue):
391 ref = self.GetChangeInfo(issue)['branch']
392 if not ref.startswith('refs/'):
393 # NOTE: it is possible to create 'refs/x' branch,
394 # aka 'refs/heads/refs/x'. However, this is ill-advised.
395 ref = 'refs/heads/%s' % ref
396 return ref
397
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000398 def GetChangeOwner(self, issue):
399 return self.GetChangeInfo(issue)['owner']['email']
400
401 def GetChangeReviewers(self, issue, approving_only=True):
Aaron Gable8b478f02017-07-31 15:33:19 -0700402 changeinfo = self.GetChangeInfo(issue)
403 if approving_only:
404 labelinfo = changeinfo.get('labels', {}).get('Code-Review', {})
405 values = labelinfo.get('values', {}).keys()
406 try:
407 max_value = max(int(v) for v in values)
408 reviewers = [r for r in labelinfo.get('all', [])
409 if r.get('value', 0) == max_value]
410 except ValueError: # values is the empty list
411 reviewers = []
412 else:
413 reviewers = changeinfo.get('reviewers', {}).get('REVIEWER', [])
414 return [r.get('email') for r in reviewers]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000415
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000416
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000417class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000418 """An instance of OutputApi gets passed to presubmit scripts so that they
419 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000420 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000421 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000422 PresubmitError = _PresubmitError
423 PresubmitPromptWarning = _PresubmitPromptWarning
424 PresubmitNotifyResult = _PresubmitNotifyResult
425 MailTextResult = _MailTextResult
426
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000427 def __init__(self, is_committing):
428 self.is_committing = is_committing
Daniel Cheng7227d212017-11-17 08:12:37 -0800429 self.more_cc = []
430
431 def AppendCC(self, cc):
432 """Appends a user to cc for this change."""
433 self.more_cc.append(cc)
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000434
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000435 def PresubmitPromptOrNotify(self, *args, **kwargs):
436 """Warn the user when uploading, but only notify if committing."""
437 if self.is_committing:
438 return self.PresubmitNotifyResult(*args, **kwargs)
439 return self.PresubmitPromptWarning(*args, **kwargs)
440
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000441
442class InputApi(object):
443 """An instance of this object is passed to presubmit scripts so they can
444 know stuff about the change they're looking at.
445 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000446 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800447 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000448
maruel@chromium.org3410d912009-06-09 20:56:16 +0000449 # File extensions that are considered source files from a style guide
450 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000451 #
452 # Files without an extension aren't included in the list. If you want to
453 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
454 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000455 DEFAULT_WHITE_LIST = (
456 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000457 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
458 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000459 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000460 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000461 # Other
Sergey Ulanov166bc4c2018-04-30 17:03:38 -0700462 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$", r".+\.mojom$",
463 r".+\.fidl$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000464 )
465
466 # Path regexp that should be excluded from being considered containing source
467 # files. Don't modify this list from a presubmit script!
468 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000469 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000470 r".*\bexperimental[\\\/].*",
Kent Tamura179dd1e2018-04-26 15:07:41 +0900471 # Exclude third_party/.* but NOT third_party/{WebKit,blink}
472 # (crbug.com/539768 and crbug.com/836555).
473 r".*\bthird_party[\\\/](?!(WebKit|blink)[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000474 # Output directories (just in case)
475 r".*\bDebug[\\\/].*",
476 r".*\bRelease[\\\/].*",
477 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000478 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000479 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000480 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000481 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000482 r"(|.*[\\\/])\.git[\\\/].*",
483 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000484 # There is no point in processing a patch file.
485 r".+\.diff$",
486 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000487 )
488
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000489 def __init__(self, change, presubmit_path, is_committing,
Edward Lesmes8e282792018-04-03 18:50:29 -0400490 verbose, gerrit_obj, dry_run=None, thread_pool=None, parallel=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000491 """Builds an InputApi object.
492
493 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000494 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000495 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000496 is_committing: True if the change is about to be committed.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000497 gerrit_obj: provides basic Gerrit codereview functionality.
498 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -0400499 parallel: if true, all tests reported via input_api.RunTests for all
500 PRESUBMIT files will be run in parallel.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000501 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000502 # Version number of the presubmit_support script.
503 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000504 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000505 self.is_committing = is_committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000506 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000507 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000508
Edward Lesmes8e282792018-04-03 18:50:29 -0400509 self.parallel = parallel
510 self.thread_pool = thread_pool or ThreadPool()
511
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000512 # We expose various modules and functions as attributes of the input_api
513 # so that presubmit scripts don't have to import them.
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +0900514 self.ast = ast
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000515 self.basename = os.path.basename
516 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000517 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000518 self.cStringIO = cStringIO
dcheng091b7db2016-06-16 01:27:51 -0700519 self.fnmatch = fnmatch
Yoshisato Yanagisawa04600b42019-03-15 03:03:41 +0000520 self.gclient_paths = gclient_paths
Yoshisato Yanagisawa57dd17b2019-03-22 09:10:29 +0000521 # TODO(yyanagisawa): stop exposing this when python3 become default.
522 # Since python3's tempfile has TemporaryDirectory, we do not need this.
523 self.temporary_directory = gclient_utils.temporary_directory
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000524 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000525 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000526 self.logging = logging.getLogger('PRESUBMIT')
Yoshisato Yanagisawa406de132018-06-29 05:43:25 +0000527 self.marshal = marshal
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000528 self.os_listdir = os.listdir
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000529 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000530 self.os_stat = os.stat
Yoshisato Yanagisawa406de132018-06-29 05:43:25 +0000531 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000532 self.pickle = pickle
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000533 self.re = re
534 self.subprocess = subprocess
535 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000536 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000537 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000538 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000539 self.urllib2 = urllib2
540
Robert Iannucci50258932018-03-19 10:30:59 -0700541 self.is_windows = sys.platform == 'win32'
542
543 # Set python_executable to 'python'. This is interpreted in CallCommand to
544 # convert to vpython in order to allow scripts in other repos (e.g. src.git)
545 # to automatically pick up that repo's .vpython file, instead of inheriting
546 # the one in depot_tools.
547 self.python_executable = 'python'
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000548 self.environ = os.environ
549
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000550 # InputApi.platform is the platform you're currently running on.
551 self.platform = sys.platform
552
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000553 self.cpu_count = multiprocessing.cpu_count()
554
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000555 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000556 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000557
558 # We carry the canned checks so presubmit scripts can easily use them.
559 self.canned_checks = presubmit_canned_checks
560
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100561 # Temporary files we must manually remove at the end of a run.
562 self._named_temporary_files = []
Jochen Eisinger72606f82017-04-04 10:44:18 +0200563
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000564 # TODO(dpranke): figure out a list of all approved owners for a repo
565 # in order to be able to handle wildcard OWNERS files?
566 self.owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +0200567 fopen=file, os_path=self.os_path)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200568 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000569 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000570 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000571
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000572 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000573 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000574 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800575 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000576 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000577 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000578 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
579 for (a, b, header) in cpplint._re_pattern_templates
580 ]
581
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000582 def PresubmitLocalPath(self):
583 """Returns the local path of the presubmit script currently being run.
584
585 This is useful if you don't want to hard-code absolute paths in the
586 presubmit script. For example, It can be used to find another file
587 relative to the PRESUBMIT.py script, so the whole tree can be branched and
588 the presubmit script still works, without editing its content.
589 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000590 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000591
agable0b65e732016-11-22 09:25:46 -0800592 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000593 """Same as input_api.change.AffectedFiles() except only lists files
594 (and optionally directories) in the same directory as the current presubmit
595 script, or subdirectories thereof.
596 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000597 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000598 if len(dir_with_slash) == 1:
599 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000600
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000601 return filter(
602 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
agable0b65e732016-11-22 09:25:46 -0800603 self.change.AffectedFiles(include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000604
agable0b65e732016-11-22 09:25:46 -0800605 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000606 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800607 paths = [af.LocalPath() for af in self.AffectedFiles()]
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000608 logging.debug("LocalPaths: %s", paths)
609 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000610
agable0b65e732016-11-22 09:25:46 -0800611 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000612 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800613 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000614
John Budorick16162372018-04-18 10:39:53 -0700615 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
agable0b65e732016-11-22 09:25:46 -0800616 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000617 in the same directory as the current presubmit script, or subdirectories
618 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000619 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000620 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800621 warn("AffectedTestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000622 " is deprecated and ignored" % str(include_deletes),
623 category=DeprecationWarning,
624 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800625 return filter(lambda x: x.IsTestableFile(),
John Budorick16162372018-04-18 10:39:53 -0700626 self.AffectedFiles(include_deletes=False, **kwargs))
agable0b65e732016-11-22 09:25:46 -0800627
628 def AffectedTextFiles(self, include_deletes=None):
629 """An alias to AffectedTestableFiles for backwards compatibility."""
630 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000631
maruel@chromium.org3410d912009-06-09 20:56:16 +0000632 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
633 """Filters out files that aren't considered "source file".
634
635 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
636 and InputApi.DEFAULT_BLACK_LIST is used respectively.
637
638 The lists will be compiled as regular expression and
639 AffectedFile.LocalPath() needs to pass both list.
640
641 Note: Copy-paste this function to suit your needs or use a lambda function.
642 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000643 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000644 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000645 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000646 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000647 return True
648 return False
649 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
650 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
651
652 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800653 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000654
655 If source_file is None, InputApi.FilterSourceFile() is used.
656 """
657 if not source_file:
658 source_file = self.FilterSourceFile
agable0b65e732016-11-22 09:25:46 -0800659 return filter(source_file, self.AffectedTestableFiles())
maruel@chromium.org3410d912009-06-09 20:56:16 +0000660
661 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000662 """An iterator over all text lines in "new" version of changed files.
663
664 Only lists lines from new or modified text files in the change that are
665 contained by the directory of the currently executing presubmit script.
666
667 This is useful for doing line-by-line regex checks, like checking for
668 trailing whitespace.
669
670 Yields:
671 a 3 tuple:
672 the AffectedFile instance of the current file;
673 integer line number (1-based); and
674 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000675
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000676 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000677 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000678 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000679 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000681 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000682 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000683
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000684 Deny reading anything outside the repository.
685 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000686 if isinstance(file_item, AffectedFile):
687 file_item = file_item.AbsoluteLocalPath()
688 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000689 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000690 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000691
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100692 def CreateTemporaryFile(self, **kwargs):
693 """Returns a named temporary file that must be removed with a call to
694 RemoveTemporaryFiles().
695
696 All keyword arguments are forwarded to tempfile.NamedTemporaryFile(),
697 except for |delete|, which is always set to False.
698
699 Presubmit checks that need to create a temporary file and pass it for
700 reading should use this function instead of NamedTemporaryFile(), as
701 Windows fails to open a file that is already open for writing.
702
703 with input_api.CreateTemporaryFile() as f:
704 f.write('xyz')
705 f.close()
706 input_api.subprocess.check_output(['script-that', '--reads-from',
707 f.name])
708
709
710 Note that callers of CreateTemporaryFile() should not worry about removing
711 any temporary file; this is done transparently by the presubmit handling
712 code.
713 """
714 if 'delete' in kwargs:
715 # Prevent users from passing |delete|; we take care of file deletion
716 # ourselves and this prevents unintuitive error messages when we pass
717 # delete=False and 'delete' is also in kwargs.
718 raise TypeError('CreateTemporaryFile() does not take a "delete" '
719 'argument, file deletion is handled automatically by '
720 'the same presubmit_support code that creates InputApi '
721 'objects.')
722 temp_file = self.tempfile.NamedTemporaryFile(delete=False, **kwargs)
723 self._named_temporary_files.append(temp_file.name)
724 return temp_file
725
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000726 @property
727 def tbr(self):
728 """Returns if a change is TBR'ed."""
Jeremy Romandce22502017-06-20 15:37:29 -0400729 return 'TBR' in self.change.tags or self.change.TBRsFromDescription()
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000730
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000731 def RunTests(self, tests_mix, parallel=True):
Edward Lesmes8e282792018-04-03 18:50:29 -0400732 # RunTests doesn't actually run tests. It adds them to a ThreadPool that
733 # will run all tests once all PRESUBMIT files are processed.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000734 tests = []
735 msgs = []
Edward Lemur7e3c67f2018-07-20 20:52:49 +0000736 parallel = parallel and self.parallel
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000737 for t in tests_mix:
Edward Lesmes8e282792018-04-03 18:50:29 -0400738 if isinstance(t, OutputApi.PresubmitResult) and t:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000739 msgs.append(t)
740 else:
741 assert issubclass(t.message, _PresubmitResult)
742 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000743 if self.verbose:
744 t.info = _PresubmitNotifyResult
Edward Lemur1037c742018-05-01 18:56:04 -0400745 if not t.kwargs.get('cwd'):
746 t.kwargs['cwd'] = self.PresubmitLocalPath()
Edward Lesmes8e282792018-04-03 18:50:29 -0400747 self.thread_pool.AddTests(tests, parallel)
Edward Lemur7e3c67f2018-07-20 20:52:49 +0000748 if not parallel:
Edward Lesmes8e282792018-04-03 18:50:29 -0400749 msgs.extend(self.thread_pool.RunAsync())
750 return msgs
scottmg86099d72016-09-01 09:16:51 -0700751
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000752
nick@chromium.orgff526192013-06-10 19:30:26 +0000753class _DiffCache(object):
754 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000755 def __init__(self, upstream=None):
756 """Stores the upstream revision against which all diffs will be computed."""
757 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000758
759 def GetDiff(self, path, local_root):
760 """Get the diff for a particular path."""
761 raise NotImplementedError()
762
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700763 def GetOldContents(self, path, local_root):
764 """Get the old version for a particular path."""
765 raise NotImplementedError()
766
nick@chromium.orgff526192013-06-10 19:30:26 +0000767
nick@chromium.orgff526192013-06-10 19:30:26 +0000768class _GitDiffCache(_DiffCache):
769 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000770 def __init__(self, upstream):
771 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000772 self._diffs_by_file = None
773
774 def GetDiff(self, path, local_root):
775 if not self._diffs_by_file:
776 # Compute a single diff for all files and parse the output; should
777 # with git this is much faster than computing one diff for each file.
778 diffs = {}
779
780 # Don't specify any filenames below, because there are command line length
781 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000782 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
783 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000784
785 # This regex matches the path twice, separated by a space. Note that
786 # filename itself may contain spaces.
787 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
788 current_diff = []
789 keep_line_endings = True
790 for x in unified_diff.splitlines(keep_line_endings):
791 match = file_marker.match(x)
792 if match:
793 # Marks the start of a new per-file section.
794 diffs[match.group('filename')] = current_diff = [x]
795 elif x.startswith('diff --git'):
796 raise PresubmitFailure('Unexpected diff line: %s' % x)
797 else:
798 current_diff.append(x)
799
800 self._diffs_by_file = dict(
801 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
802
803 if path not in self._diffs_by_file:
804 raise PresubmitFailure(
805 'Unified diff did not contain entry for file %s' % path)
806
807 return self._diffs_by_file[path]
808
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700809 def GetOldContents(self, path, local_root):
810 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
811
nick@chromium.orgff526192013-06-10 19:30:26 +0000812
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000813class AffectedFile(object):
814 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000815
816 DIFF_CACHE = _DiffCache
817
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000818 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800819 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000820 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000821 self._path = path
822 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000823 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000824 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000825 self._cached_changed_contents = None
826 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000827 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700828 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000829
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000830 def LocalPath(self):
831 """Returns the path of this file on the local disk relative to client root.
Andrew Grieve92b8b992017-11-02 09:42:24 -0400832
833 This should be used for error messages but not for accessing files,
834 because presubmit checks are run with CWD=PresubmitLocalPath() (which is
835 often != client root).
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000836 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000837 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000838
839 def AbsoluteLocalPath(self):
840 """Returns the absolute path of this file on the local disk.
841 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000842 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000843
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000844 def Action(self):
845 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000846 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000847
agable0b65e732016-11-22 09:25:46 -0800848 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000849 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000850
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000851 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000852 raise NotImplementedError() # Implement when needed
853
agable0b65e732016-11-22 09:25:46 -0800854 def IsTextFile(self):
855 """An alias to IsTestableFile for backwards compatibility."""
856 return self.IsTestableFile()
857
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700858 def OldContents(self):
859 """Returns an iterator over the lines in the old version of file.
860
Daniel Cheng2da34fe2017-03-21 20:42:12 -0700861 The old version is the file before any modifications in the user's
862 workspace, i.e. the "left hand side".
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700863
864 Contents will be empty if the file is a directory or does not exist.
865 Note: The carriage returns (LF or CR) are stripped off.
866 """
867 return self._diff_cache.GetOldContents(self.LocalPath(),
868 self._local_root).splitlines()
869
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000870 def NewContents(self):
871 """Returns an iterator over the lines in the new version of file.
872
873 The new version is the file in the user's workspace, i.e. the "right hand
874 side".
875
876 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000877 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000878 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000879 if self._cached_new_contents is None:
880 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800881 try:
882 self._cached_new_contents = gclient_utils.FileRead(
883 self.AbsoluteLocalPath(), 'rU').splitlines()
884 except IOError:
885 pass # File not found? That's fine; maybe it was deleted.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000886 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000887
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000888 def ChangedContents(self):
889 """Returns a list of tuples (line number, line text) of all new lines.
890
891 This relies on the scm diff output describing each changed code section
892 with a line of the form
893
894 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
895 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000896 if self._cached_changed_contents is not None:
897 return self._cached_changed_contents[:]
898 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000899 line_num = 0
900
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000901 for line in self.GenerateScmDiff().splitlines():
902 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
903 if m:
904 line_num = int(m.groups(1)[0])
905 continue
906 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000907 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000908 if not line.startswith('-'):
909 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000910 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000911
maruel@chromium.org5de13972009-06-10 18:16:06 +0000912 def __str__(self):
913 return self.LocalPath()
914
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000915 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000916 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000917
maruel@chromium.org58407af2011-04-12 23:15:57 +0000918
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000919class GitAffectedFile(AffectedFile):
920 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000921 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800922 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000923
nick@chromium.orgff526192013-06-10 19:30:26 +0000924 DIFF_CACHE = _GitDiffCache
925
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000926 def __init__(self, *args, **kwargs):
927 AffectedFile.__init__(self, *args, **kwargs)
928 self._server_path = None
agable0b65e732016-11-22 09:25:46 -0800929 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000930
agable0b65e732016-11-22 09:25:46 -0800931 def IsTestableFile(self):
932 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000933 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -0800934 # A deleted file is not testable.
935 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000936 else:
agable0b65e732016-11-22 09:25:46 -0800937 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
938 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000939
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000940
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000941class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000942 """Describe a change.
943
944 Used directly by the presubmit scripts to query the current change being
945 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000946
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000947 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000948 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000949 self.KEY: equivalent to tags['KEY']
950 """
951
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000952 _AFFECTED_FILES = AffectedFile
953
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000954 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000955 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000956 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000957 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000958
maruel@chromium.org58407af2011-04-12 23:15:57 +0000959 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000960 self, name, description, local_root, files, issue, patchset, author,
961 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000962 if files is None:
963 files = []
964 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000965 # Convert root into an absolute path.
966 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000967 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000968 self.issue = issue
969 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000970 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000971
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000972 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000973 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000974 self._description_without_tags = ''
975 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000976
maruel@chromium.orge085d812011-10-10 19:49:15 +0000977 assert all(
978 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
979
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000980 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000981 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000982 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
983 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000984 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000985
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000986 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000987 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000988 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000989
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000990 def DescriptionText(self):
991 """Returns the user-entered changelist description, minus tags.
992
993 Any line in the user-provided description starting with e.g. "FOO="
994 (whitespace permitted before and around) is considered a tag line. Such
995 lines are stripped out of the description this function returns.
996 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000997 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998
999 def FullDescriptionText(self):
1000 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001001 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001002
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001003 def SetDescriptionText(self, description):
1004 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001005
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001006 Also updates the list of tags."""
1007 self._full_description = description
1008
1009 # From the description text, build up a dictionary of key/value pairs
1010 # plus the description minus all key/value or "tag" lines.
1011 description_without_tags = []
1012 self.tags = {}
1013 for line in self._full_description.splitlines():
1014 m = self.TAG_LINE_RE.match(line)
1015 if m:
1016 self.tags[m.group('key')] = m.group('value')
1017 else:
1018 description_without_tags.append(line)
1019
1020 # Change back to text and remove whitespace at end.
1021 self._description_without_tags = (
1022 '\n'.join(description_without_tags).rstrip())
1023
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001025 """Returns the repository (checkout) root directory for this change,
1026 as an absolute path.
1027 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001028 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001029
1030 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001031 """Return tags directly as attributes on the object."""
1032 if not re.match(r"^[A-Z_]*$", attr):
1033 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +00001034 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001035
Aaron Gablefc03e672017-05-15 14:09:42 -07001036 def BugsFromDescription(self):
1037 """Returns all bugs referenced in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -07001038 tags = [b.strip() for b in self.tags.get('BUG', '').split(',') if b.strip()]
Caleb Rouleauc0546b92019-02-22 06:12:57 +00001039 footers = []
1040 unsplit_footers = git_footers.parse_footers(self._full_description).get(
1041 'Bug', [])
1042 for unsplit_footer in unsplit_footers:
1043 footers += [b.strip() for b in unsplit_footer.split(',')]
Aaron Gable12ef5012017-05-15 14:29:00 -07001044 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -07001045
1046 def ReviewersFromDescription(self):
1047 """Returns all reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -07001048 # We don't support a "R:" git-footer for reviewers; that is in metadata.
1049 tags = [r.strip() for r in self.tags.get('R', '').split(',') if r.strip()]
1050 return sorted(set(tags))
Aaron Gablefc03e672017-05-15 14:09:42 -07001051
1052 def TBRsFromDescription(self):
1053 """Returns all TBR reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -07001054 tags = [r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()]
1055 # TODO(agable): Remove support for 'Tbr:' when TBRs are programmatically
1056 # determined by self-CR+1s.
1057 footers = git_footers.parse_footers(self._full_description).get('Tbr', [])
1058 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -07001059
1060 # TODO(agable): Delete these once we're sure they're unused.
1061 @property
1062 def BUG(self):
1063 return ','.join(self.BugsFromDescription())
1064 @property
1065 def R(self):
1066 return ','.join(self.ReviewersFromDescription())
1067 @property
1068 def TBR(self):
1069 return ','.join(self.TBRsFromDescription())
1070
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001071 def AllFiles(self, root=None):
1072 """List all files under source control in the repo."""
1073 raise NotImplementedError()
1074
agable0b65e732016-11-22 09:25:46 -08001075 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001076 """Returns a list of AffectedFile instances for all files in the change.
1077
1078 Args:
1079 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +00001080 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001081
1082 Returns:
1083 [AffectedFile(path, action), AffectedFile(path, action)]
1084 """
agable0b65e732016-11-22 09:25:46 -08001085 affected = filter(file_filter, self._affected_files)
sail@chromium.org5538e022011-05-12 17:53:16 +00001086
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001087 if include_deletes:
1088 return affected
Lei Zhang9611c4c2017-04-04 01:41:56 -07001089 return filter(lambda x: x.Action() != 'D', affected)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001090
John Budorick16162372018-04-18 10:39:53 -07001091 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +00001092 """Return a list of the existing text files in a change."""
1093 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -08001094 warn("AffectedTeestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001095 " is deprecated and ignored" % str(include_deletes),
1096 category=DeprecationWarning,
1097 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -08001098 return filter(lambda x: x.IsTestableFile(),
John Budorick16162372018-04-18 10:39:53 -07001099 self.AffectedFiles(include_deletes=False, **kwargs))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001100
agable0b65e732016-11-22 09:25:46 -08001101 def AffectedTextFiles(self, include_deletes=None):
1102 """An alias to AffectedTestableFiles for backwards compatibility."""
1103 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001104
agable0b65e732016-11-22 09:25:46 -08001105 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001106 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001107 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001108
agable0b65e732016-11-22 09:25:46 -08001109 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001110 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001111 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001112
1113 def RightHandSideLines(self):
1114 """An iterator over all text lines in "new" version of changed files.
1115
1116 Lists lines from new or modified text files in the change.
1117
1118 This is useful for doing line-by-line regex checks, like checking for
1119 trailing whitespace.
1120
1121 Yields:
1122 a 3 tuple:
1123 the AffectedFile instance of the current file;
1124 integer line number (1-based); and
1125 the contents of the line as a string.
1126 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001127 return _RightHandSideLinesImpl(
1128 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -08001129 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001130
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02001131 def OriginalOwnersFiles(self):
1132 """A map from path names of affected OWNERS files to their old content."""
1133 def owners_file_filter(f):
1134 return 'OWNERS' in os.path.split(f.LocalPath())[1]
1135 files = self.AffectedFiles(file_filter=owners_file_filter)
1136 return dict([(f.LocalPath(), f.OldContents()) for f in files])
1137
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001138
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001139class GitChange(Change):
1140 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001141 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001142
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001143 def AllFiles(self, root=None):
1144 """List all files under source control in the repo."""
1145 root = root or self.RepositoryRoot()
1146 return subprocess.check_output(
Aaron Gable7817f022017-12-12 09:43:17 -08001147 ['git', '-c', 'core.quotePath=false', 'ls-files', '--', '.'],
1148 cwd=root).splitlines()
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001149
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001150
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001151def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001152 """Finds all presubmit files that apply to a given set of source files.
1153
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001154 If inherit-review-settings-ok is present right under root, looks for
1155 PRESUBMIT.py in directories enclosing root.
1156
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001157 Args:
1158 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001159 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001160
1161 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001162 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001163 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001164 files = [normpath(os.path.join(root, f)) for f in files]
1165
1166 # List all the individual directories containing files.
1167 directories = set([os.path.dirname(f) for f in files])
1168
1169 # Ignore root if inherit-review-settings-ok is present.
1170 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1171 root = None
1172
1173 # Collect all unique directories that may contain PRESUBMIT.py.
1174 candidates = set()
1175 for directory in directories:
1176 while True:
1177 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001178 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001179 candidates.add(directory)
1180 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001181 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001182 parent_dir = os.path.dirname(directory)
1183 if parent_dir == directory:
1184 # We hit the system root directory.
1185 break
1186 directory = parent_dir
1187
1188 # Look for PRESUBMIT.py in all candidate directories.
1189 results = []
1190 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -07001191 try:
1192 for f in os.listdir(directory):
1193 p = os.path.join(directory, f)
1194 if os.path.isfile(p) and re.match(
1195 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1196 results.append(p)
1197 except OSError:
1198 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001199
tobiasjs2836bcf2016-08-16 04:08:16 -07001200 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001201 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001202
1203
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001204class GetTryMastersExecuter(object):
1205 @staticmethod
1206 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1207 """Executes GetPreferredTryMasters() from a single presubmit script.
1208
1209 Args:
1210 script_text: The text of the presubmit script.
1211 presubmit_path: Project script to run.
1212 project: Project name to pass to presubmit script for bot selection.
1213
1214 Return:
1215 A map of try masters to map of builders to set of tests.
1216 """
1217 context = {}
1218 try:
1219 exec script_text in context
1220 except Exception, e:
1221 raise PresubmitFailure('"%s" had an exception.\n%s'
1222 % (presubmit_path, e))
1223
1224 function_name = 'GetPreferredTryMasters'
1225 if function_name not in context:
1226 return {}
1227 get_preferred_try_masters = context[function_name]
1228 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1229 raise PresubmitFailure(
1230 'Expected function "GetPreferredTryMasters" to take two arguments.')
1231 return get_preferred_try_masters(project, change)
1232
1233
rmistry@google.com5626a922015-02-26 14:03:30 +00001234class GetPostUploadExecuter(object):
1235 @staticmethod
1236 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1237 """Executes PostUploadHook() from a single presubmit script.
1238
1239 Args:
1240 script_text: The text of the presubmit script.
1241 presubmit_path: Project script to run.
1242 cl: The Changelist object.
1243 change: The Change object.
1244
1245 Return:
1246 A list of results objects.
1247 """
1248 context = {}
1249 try:
1250 exec script_text in context
1251 except Exception, e:
1252 raise PresubmitFailure('"%s" had an exception.\n%s'
1253 % (presubmit_path, e))
1254
1255 function_name = 'PostUploadHook'
1256 if function_name not in context:
1257 return {}
1258 post_upload_hook = context[function_name]
1259 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1260 raise PresubmitFailure(
1261 'Expected function "PostUploadHook" to take three arguments.')
1262 return post_upload_hook(cl, change, OutputApi(False))
1263
1264
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001265def _MergeMasters(masters1, masters2):
1266 """Merges two master maps. Merges also the tests of each builder."""
1267 result = {}
1268 for (master, builders) in itertools.chain(masters1.iteritems(),
1269 masters2.iteritems()):
1270 new_builders = result.setdefault(master, {})
1271 for (builder, tests) in builders.iteritems():
1272 new_builders.setdefault(builder, set([])).update(tests)
1273 return result
1274
1275
1276def DoGetTryMasters(change,
1277 changed_files,
1278 repository_root,
1279 default_presubmit,
1280 project,
1281 verbose,
1282 output_stream):
1283 """Get the list of try masters from the presubmit scripts.
1284
1285 Args:
1286 changed_files: List of modified files.
1287 repository_root: The repository root.
1288 default_presubmit: A default presubmit script to execute in any case.
1289 project: Optional name of a project used in selecting trybots.
1290 verbose: Prints debug info.
1291 output_stream: A stream to write debug output to.
1292
1293 Return:
1294 Map of try masters to map of builders to set of tests.
1295 """
1296 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1297 if not presubmit_files and verbose:
1298 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1299 results = {}
1300 executer = GetTryMastersExecuter()
1301
1302 if default_presubmit:
1303 if verbose:
1304 output_stream.write("Running default presubmit script.\n")
1305 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1306 results = _MergeMasters(results, executer.ExecPresubmitScript(
1307 default_presubmit, fake_path, project, change))
1308 for filename in presubmit_files:
1309 filename = os.path.abspath(filename)
1310 if verbose:
1311 output_stream.write("Running %s\n" % filename)
1312 # Accept CRLF presubmit script.
1313 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1314 results = _MergeMasters(results, executer.ExecPresubmitScript(
1315 presubmit_script, filename, project, change))
1316
1317 # Make sets to lists again for later JSON serialization.
1318 for builders in results.itervalues():
1319 for builder in builders:
1320 builders[builder] = list(builders[builder])
1321
1322 if results and verbose:
1323 output_stream.write('%s\n' % str(results))
1324 return results
1325
1326
rmistry@google.com5626a922015-02-26 14:03:30 +00001327def DoPostUploadExecuter(change,
1328 cl,
1329 repository_root,
1330 verbose,
1331 output_stream):
1332 """Execute the post upload hook.
1333
1334 Args:
1335 change: The Change object.
1336 cl: The Changelist object.
1337 repository_root: The repository root.
1338 verbose: Prints debug info.
1339 output_stream: A stream to write debug output to.
1340 """
1341 presubmit_files = ListRelevantPresubmitFiles(
1342 change.LocalPaths(), repository_root)
1343 if not presubmit_files and verbose:
1344 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1345 results = []
1346 executer = GetPostUploadExecuter()
1347 # The root presubmit file should be executed after the ones in subdirectories.
1348 # i.e. the specific post upload hooks should run before the general ones.
1349 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1350 presubmit_files.reverse()
1351
1352 for filename in presubmit_files:
1353 filename = os.path.abspath(filename)
1354 if verbose:
1355 output_stream.write("Running %s\n" % filename)
1356 # Accept CRLF presubmit script.
1357 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1358 results.extend(executer.ExecPresubmitScript(
1359 presubmit_script, filename, cl, change))
1360 output_stream.write('\n')
1361 if results:
1362 output_stream.write('** Post Upload Hook Messages **\n')
1363 for result in results:
1364 result.handle(output_stream)
1365 output_stream.write('\n')
1366
1367 return results
1368
1369
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001370class PresubmitExecuter(object):
Aaron Gable668c1d82018-04-03 10:19:16 -07001371 def __init__(self, change, committing, verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04001372 gerrit_obj, dry_run=None, thread_pool=None, parallel=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001373 """
1374 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001375 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001376 committing: True if 'git cl land' is running, False if 'git cl upload' is.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001377 gerrit_obj: provides basic Gerrit codereview functionality.
1378 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001379 parallel: if true, all tests reported via input_api.RunTests for all
1380 PRESUBMIT files will be run in parallel.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001381 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001382 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001383 self.committing = committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001384 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001385 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001386 self.dry_run = dry_run
Daniel Cheng7227d212017-11-17 08:12:37 -08001387 self.more_cc = []
Edward Lesmes8e282792018-04-03 18:50:29 -04001388 self.thread_pool = thread_pool
1389 self.parallel = parallel
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001390
1391 def ExecPresubmitScript(self, script_text, presubmit_path):
1392 """Executes a single presubmit script.
1393
1394 Args:
1395 script_text: The text of the presubmit script.
1396 presubmit_path: The path to the presubmit file (this will be reported via
1397 input_api.PresubmitLocalPath()).
1398
1399 Return:
1400 A list of result objects, empty if no problems.
1401 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001402
chase@chromium.org8e416c82009-10-06 04:30:44 +00001403 # Change to the presubmit file's directory to support local imports.
1404 main_path = os.getcwd()
1405 os.chdir(os.path.dirname(presubmit_path))
1406
1407 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001408 input_api = InputApi(self.change, presubmit_path, self.committing,
Aaron Gable668c1d82018-04-03 10:19:16 -07001409 self.verbose, gerrit_obj=self.gerrit,
Edward Lesmes8e282792018-04-03 18:50:29 -04001410 dry_run=self.dry_run, thread_pool=self.thread_pool,
1411 parallel=self.parallel)
Daniel Cheng7227d212017-11-17 08:12:37 -08001412 output_api = OutputApi(self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001413 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001414 try:
1415 exec script_text in context
1416 except Exception, e:
1417 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001418
1419 # These function names must change if we make substantial changes to
1420 # the presubmit API that are not backwards compatible.
1421 if self.committing:
1422 function_name = 'CheckChangeOnCommit'
1423 else:
1424 function_name = 'CheckChangeOnUpload'
1425 if function_name in context:
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +01001426 try:
Daniel Cheng7227d212017-11-17 08:12:37 -08001427 context['__args'] = (input_api, output_api)
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +01001428 logging.debug('Running %s in %s', function_name, presubmit_path)
1429 result = eval(function_name + '(*__args)', context)
1430 logging.debug('Running %s done.', function_name)
Daniel Chengd36fce42017-11-21 21:52:52 -08001431 self.more_cc.extend(output_api.more_cc)
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +01001432 finally:
1433 map(os.remove, input_api._named_temporary_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001434 if not (isinstance(result, types.TupleType) or
1435 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001436 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001437 'Presubmit functions must return a tuple or list')
1438 for item in result:
1439 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001440 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001441 'All presubmit results must be of types derived from '
1442 'output_api.PresubmitResult')
1443 else:
1444 result = () # no error since the script doesn't care about current event.
1445
chase@chromium.org8e416c82009-10-06 04:30:44 +00001446 # Return the process to the original working directory.
1447 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001448 return result
1449
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001450def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001451 committing,
1452 verbose,
1453 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001454 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001455 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001456 may_prompt,
Aaron Gable668c1d82018-04-03 10:19:16 -07001457 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04001458 dry_run=None,
1459 parallel=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001460 """Runs all presubmit checks that apply to the files in the change.
1461
1462 This finds all PRESUBMIT.py files in directories enclosing the files in the
1463 change (up to the repository root) and calls the relevant entrypoint function
1464 depending on whether the change is being committed or uploaded.
1465
1466 Prints errors, warnings and notifications. Prompts the user for warnings
1467 when needed.
1468
1469 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001470 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001471 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001472 verbose: Prints debug info.
1473 output_stream: A stream to write output from presubmit tests to.
1474 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001475 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001476 may_prompt: Enable (y/n) questions on warning or error. If False,
1477 any questions are answered with yes by default.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001478 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001479 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001480 parallel: if true, all tests specified by input_api.RunTests in all
1481 PRESUBMIT files will be run in parallel.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001482
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001483 Warning:
1484 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1485 SHOULD be sys.stdin.
1486
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001487 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001488 A PresubmitOutput object. Use output.should_continue() to figure out
1489 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001490 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001491 old_environ = os.environ
1492 try:
1493 # Make sure python subprocesses won't generate .pyc files.
1494 os.environ = os.environ.copy()
1495 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001496
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001497 output = PresubmitOutput(input_stream, output_stream)
1498 if committing:
1499 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001500 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001501 output.write("Running presubmit upload checks ...\n")
1502 start_time = time.time()
1503 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001504 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001505 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001506 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001507 results = []
Edward Lesmes8e282792018-04-03 18:50:29 -04001508 thread_pool = ThreadPool()
Edward Lemur7e3c67f2018-07-20 20:52:49 +00001509 executer = PresubmitExecuter(change, committing, verbose, gerrit_obj,
1510 dry_run, thread_pool, parallel)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001511 if default_presubmit:
1512 if verbose:
1513 output.write("Running default presubmit script.\n")
1514 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1515 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1516 for filename in presubmit_files:
1517 filename = os.path.abspath(filename)
1518 if verbose:
1519 output.write("Running %s\n" % filename)
1520 # Accept CRLF presubmit script.
1521 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1522 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001523
Edward Lesmes8e282792018-04-03 18:50:29 -04001524 results += thread_pool.RunAsync()
1525
Daniel Cheng7227d212017-11-17 08:12:37 -08001526 output.more_cc.extend(executer.more_cc)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001527 errors = []
1528 notifications = []
1529 warnings = []
1530 for result in results:
1531 if result.fatal:
1532 errors.append(result)
1533 elif result.should_prompt:
1534 warnings.append(result)
1535 else:
1536 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001537
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001538 output.write('\n')
1539 for name, items in (('Messages', notifications),
1540 ('Warnings', warnings),
1541 ('ERRORS', errors)):
1542 if items:
1543 output.write('** Presubmit %s **\n' % name)
1544 for item in items:
1545 item.handle(output)
1546 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001547
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001548 total_time = time.time() - start_time
1549 if total_time > 1.0:
1550 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001551
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001552 if errors:
1553 output.fail()
1554 elif warnings:
1555 output.write('There were presubmit warnings. ')
1556 if may_prompt:
1557 output.prompt_yes_no('Are you sure you wish to continue? (y/N): ')
1558 else:
1559 output.write('Presubmit checks passed.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001560
1561 global _ASKED_FOR_FEEDBACK
1562 # Ask for feedback one time out of 5.
1563 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001564 output.write(
1565 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1566 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1567 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001568 _ASKED_FOR_FEEDBACK = True
1569 return output
1570 finally:
1571 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001572
1573
1574def ScanSubDirs(mask, recursive):
1575 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001576 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001577
1578 results = []
1579 for root, dirs, files in os.walk('.'):
1580 if '.svn' in dirs:
1581 dirs.remove('.svn')
1582 if '.git' in dirs:
1583 dirs.remove('.git')
1584 for name in files:
1585 if fnmatch.fnmatch(name, mask):
1586 results.append(os.path.join(root, name))
1587 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001588
1589
1590def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001591 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001592 files = []
1593 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001594 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001595 return files
1596
1597
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001598def load_files(options, args):
1599 """Tries to determine the SCM."""
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001600 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001601 if args:
1602 files = ParseFiles(args, options.recursive)
agable0b65e732016-11-22 09:25:46 -08001603 change_scm = scm.determine_scm(options.root)
1604 if change_scm == 'git':
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001605 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001606 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001607 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001608 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001609 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001610 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001611 if not files:
1612 return None, None
1613 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001614 return change_class, files
1615
1616
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001617@contextlib.contextmanager
1618def canned_check_filter(method_names):
1619 filtered = {}
1620 try:
1621 for method_name in method_names:
1622 if not hasattr(presubmit_canned_checks, method_name):
Aaron Gableecee74c2018-04-02 15:13:08 -07001623 logging.warn('Skipping unknown "canned" check %s' % method_name)
1624 continue
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001625 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1626 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1627 yield
1628 finally:
1629 for name, method in filtered.iteritems():
1630 setattr(presubmit_canned_checks, name, method)
1631
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001632
sbc@chromium.org013731e2015-02-26 18:28:43 +00001633def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001634 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001635 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001636 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001637 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001638 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1639 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001640 parser.add_option("-r", "--recursive", action="store_true",
1641 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001642 parser.add_option("-v", "--verbose", action="count", default=0,
1643 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001644 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001645 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001646 parser.add_option("--description", default='')
1647 parser.add_option("--issue", type='int', default=0)
1648 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001649 parser.add_option("--root", default=os.getcwd(),
1650 help="Search for PRESUBMIT.py up to this directory. "
1651 "If inherit-review-settings-ok is present in this "
1652 "directory, parent directories up to the root file "
1653 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001654 parser.add_option("--upstream",
1655 help="Git only: the base ref or upstream branch against "
1656 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001657 parser.add_option("--default_presubmit")
1658 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001659 parser.add_option("--skip_canned", action='append', default=[],
1660 help="A list of checks to skip which appear in "
1661 "presubmit_canned_checks. Can be provided multiple times "
1662 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001663 parser.add_option("--dry_run", action='store_true',
1664 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001665 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001666 parser.add_option("--gerrit_fetch", action='store_true',
1667 help=optparse.SUPPRESS_HELP)
Edward Lesmes8e282792018-04-03 18:50:29 -04001668 parser.add_option('--parallel', action='store_true',
1669 help='Run all tests specified by input_api.RunTests in all '
1670 'PRESUBMIT files in parallel.')
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001671
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001672 options, args = parser.parse_args(argv)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001673
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001674 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001675 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001676 elif options.verbose:
1677 logging.basicConfig(level=logging.INFO)
1678 else:
1679 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001680
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001681 change_class, files = load_files(options, args)
1682 if not change_class:
1683 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001684 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001685
Aaron Gable668c1d82018-04-03 10:19:16 -07001686 gerrit_obj = None
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001687 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001688 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001689 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1690 options.author = gerrit_obj.GetChangeOwner(options.issue)
1691 options.description = gerrit_obj.GetChangeDescription(options.issue,
1692 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001693 logging.info('Got author: "%s"', options.author)
1694 logging.info('Got description: """\n%s\n"""', options.description)
1695
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001696 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001697 with canned_check_filter(options.skip_canned):
1698 results = DoPresubmitChecks(
1699 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001700 options.description,
1701 options.root,
1702 files,
1703 options.issue,
1704 options.patchset,
1705 options.author,
1706 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001707 options.commit,
1708 options.verbose,
1709 sys.stdout,
1710 sys.stdin,
1711 options.default_presubmit,
1712 options.may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001713 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04001714 options.dry_run,
1715 options.parallel)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001716 return not results.should_continue()
1717 except PresubmitFailure, e:
1718 print >> sys.stderr, e
1719 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001720 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001721
1722
1723if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001724 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001725 try:
1726 sys.exit(main())
1727 except KeyboardInterrupt:
1728 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001729 sys.exit(2)