blob: 2375d7635c48fb78491e051f23afc92dbdd02dda [file] [log] [blame]
Josip Sokcevic4de5dea2022-03-23 21:15:14 +00001#!/usr/bin/env python3
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
Raul Tambre80ee78e2019-05-06 22:41:05 +00009from __future__ import print_function
10
Saagar Sanghavi99816902020-08-11 22:41:25 +000011__version__ = '2.0.0'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000012
13# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
14# caching (between all different invocations of presubmit scripts for a given
15# change). We should add it as our presubmit scripts start feeling slow.
16
Edward Lemura5799e32020-01-17 19:26:51 +000017import argparse
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +090018import ast # Exposed through the API.
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +000019import contextlib
Yoshisato Yanagisawa406de132018-06-29 05:43:25 +000020import cpplint
dcheng091b7db2016-06-16 01:27:51 -070021import fnmatch # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000022import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000023import inspect
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +000024import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000025import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000026import logging
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000027import multiprocessing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000028import os # Somewhat exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000029import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000030import re # Exposed through the API.
Edward Lesmes8e282792018-04-03 18:50:29 -040031import signal
Allen Webbfe7d7092021-05-18 02:05:49 +000032import six
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000033import sys # Parts exposed through API.
34import tempfile # Exposed through the API.
Edward Lesmes8e282792018-04-03 18:50:29 -040035import threading
jam@chromium.org2a891dc2009-08-20 20:33:37 +000036import time
Edward Lemurde9e3ca2019-10-24 21:13:31 +000037import traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +000038import unittest # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000039from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000040
41# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000042import fix_encoding
Yoshisato Yanagisawa04600b42019-03-15 03:03:41 +000043import gclient_paths # Exposed through the API
44import gclient_utils
Josip Sokcevic7958e302023-03-01 23:02:21 +000045import git_footers
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000046import gerrit_util
Edward Lesmes9ce03f82021-01-12 20:13:31 +000047import owners_client
Jochen Eisinger76f5fc62017-04-07 16:27:46 +020048import owners_finder
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000049import presubmit_canned_checks
Saagar Sanghavi9949ab72020-07-20 20:56:40 +000050import rdb_wrapper
Josip Sokcevic7958e302023-03-01 23:02:21 +000051import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000052import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053
Edward Lemur16af3562019-10-17 22:11:33 +000054if sys.version_info.major == 2:
55 # TODO(1009814): Expose urllib2 only through urllib_request and urllib_error
56 import urllib2 # Exposed through the API.
57 import urlparse
58 import urllib2 as urllib_request
59 import urllib2 as urllib_error
60else:
61 import urllib.parse as urlparse
62 import urllib.request as urllib_request
63 import urllib.error as urllib_error
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000064
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000065# Ask for feedback only once in program lifetime.
66_ASKED_FOR_FEEDBACK = False
67
Bruce Dawsondca14bc2022-09-15 20:59:38 +000068# Set if super-verbose mode is requested, for tracking where presubmit messages
69# are coming from.
70_SHOW_CALLSTACKS = False
71
72
Edward Lemurecc27072020-01-06 16:42:34 +000073def time_time():
74 # Use this so that it can be mocked in tests without interfering with python
75 # system machinery.
76 return time.time()
77
78
maruel@chromium.org899e1c12011-04-07 17:03:18 +000079class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000080 pass
81
82
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000083class CommandData(object):
Edward Lemur940c2822019-08-23 00:34:25 +000084 def __init__(self, name, cmd, kwargs, message, python3=False):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000085 self.name = name
86 self.cmd = cmd
Edward Lesmes8e282792018-04-03 18:50:29 -040087 self.stdin = kwargs.get('stdin', None)
Edward Lemur2d6b67c2019-08-23 22:25:41 +000088 self.kwargs = kwargs.copy()
Edward Lesmes8e282792018-04-03 18:50:29 -040089 self.kwargs['stdout'] = subprocess.PIPE
90 self.kwargs['stderr'] = subprocess.STDOUT
91 self.kwargs['stdin'] = subprocess.PIPE
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000092 self.message = message
93 self.info = None
Edward Lemur940c2822019-08-23 00:34:25 +000094 self.python3 = python3
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000095
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000096
Edward Lesmes8e282792018-04-03 18:50:29 -040097# Adapted from
98# https://github.com/google/gtest-parallel/blob/master/gtest_parallel.py#L37
99#
100# An object that catches SIGINT sent to the Python process and notices
101# if processes passed to wait() die by SIGINT (we need to look for
102# both of those cases, because pressing Ctrl+C can result in either
103# the main process or one of the subprocesses getting the signal).
104#
105# Before a SIGINT is seen, wait(p) will simply call p.wait() and
106# return the result. Once a SIGINT has been seen (in the main process
107# or a subprocess, including the one the current call is waiting for),
Edward Lemur9a5bb612019-09-26 02:01:52 +0000108# wait(p) will call p.terminate().
Edward Lesmes8e282792018-04-03 18:50:29 -0400109class SigintHandler(object):
Edward Lesmes8e282792018-04-03 18:50:29 -0400110 sigint_returncodes = {-signal.SIGINT, # Unix
111 -1073741510, # Windows
112 }
113 def __init__(self):
114 self.__lock = threading.Lock()
115 self.__processes = set()
116 self.__got_sigint = False
Edward Lemur9a5bb612019-09-26 02:01:52 +0000117 self.__previous_signal = signal.signal(signal.SIGINT, self.interrupt)
Edward Lesmes8e282792018-04-03 18:50:29 -0400118
119 def __on_sigint(self):
120 self.__got_sigint = True
121 while self.__processes:
122 try:
123 self.__processes.pop().terminate()
124 except OSError:
125 pass
126
Edward Lemur9a5bb612019-09-26 02:01:52 +0000127 def interrupt(self, signal_num, frame):
Edward Lesmes8e282792018-04-03 18:50:29 -0400128 with self.__lock:
129 self.__on_sigint()
Edward Lemur9a5bb612019-09-26 02:01:52 +0000130 self.__previous_signal(signal_num, frame)
Edward Lesmes8e282792018-04-03 18:50:29 -0400131
132 def got_sigint(self):
133 with self.__lock:
134 return self.__got_sigint
135
136 def wait(self, p, stdin):
137 with self.__lock:
138 if self.__got_sigint:
139 p.terminate()
140 self.__processes.add(p)
141 stdout, stderr = p.communicate(stdin)
142 code = p.returncode
143 with self.__lock:
144 self.__processes.discard(p)
145 if code in self.sigint_returncodes:
146 self.__on_sigint()
Edward Lesmes8e282792018-04-03 18:50:29 -0400147 return stdout, stderr
148
149sigint_handler = SigintHandler()
150
151
Edward Lemurecc27072020-01-06 16:42:34 +0000152class Timer(object):
153 def __init__(self, timeout, fn):
154 self.completed = False
155 self._fn = fn
156 self._timer = threading.Timer(timeout, self._onTimer) if timeout else None
157
158 def __enter__(self):
159 if self._timer:
160 self._timer.start()
161 return self
162
163 def __exit__(self, _type, _value, _traceback):
164 if self._timer:
165 self._timer.cancel()
166
167 def _onTimer(self):
168 self._fn()
169 self.completed = True
170
171
Edward Lesmes8e282792018-04-03 18:50:29 -0400172class ThreadPool(object):
Edward Lemurecc27072020-01-06 16:42:34 +0000173 def __init__(self, pool_size=None, timeout=None):
174 self.timeout = timeout
Edward Lesmes8e282792018-04-03 18:50:29 -0400175 self._pool_size = pool_size or multiprocessing.cpu_count()
Bruce Dawson8254f062022-06-17 17:33:08 +0000176 if sys.platform == 'win32':
177 # TODO(crbug.com/1190269) - we can't use more than 56 child processes on
178 # Windows or Python3 may hang.
179 self._pool_size = min(self._pool_size, 56)
Edward Lesmes8e282792018-04-03 18:50:29 -0400180 self._messages = []
181 self._messages_lock = threading.Lock()
182 self._tests = []
183 self._tests_lock = threading.Lock()
184 self._nonparallel_tests = []
185
Edward Lemurecc27072020-01-06 16:42:34 +0000186 def _GetCommand(self, test):
Edward Lemur940c2822019-08-23 00:34:25 +0000187 vpython = 'vpython'
188 if test.python3:
189 vpython += '3'
190 if sys.platform == 'win32':
191 vpython += '.bat'
Edward Lesmes8e282792018-04-03 18:50:29 -0400192
193 cmd = test.cmd
194 if cmd[0] == 'python':
195 cmd = list(cmd)
196 cmd[0] = vpython
197 elif cmd[0].endswith('.py'):
198 cmd = [vpython] + cmd
199
Edward Lemur336e51f2019-11-14 21:42:04 +0000200 # On Windows, scripts on the current directory take precedence over PATH, so
201 # that when testing depot_tools on Windows, calling `vpython.bat` will
202 # execute the copy of vpython of the depot_tools under test instead of the
203 # one in the bot.
204 # As a workaround, we run the tests from the parent directory instead.
205 if (cmd[0] == vpython and
206 'cwd' in test.kwargs and
207 os.path.basename(test.kwargs['cwd']) == 'depot_tools'):
208 test.kwargs['cwd'] = os.path.dirname(test.kwargs['cwd'])
209 cmd[1] = os.path.join('depot_tools', cmd[1])
210
Edward Lemurecc27072020-01-06 16:42:34 +0000211 return cmd
212
213 def _RunWithTimeout(self, cmd, stdin, kwargs):
214 p = subprocess.Popen(cmd, **kwargs)
215 with Timer(self.timeout, p.terminate) as timer:
216 stdout, _ = sigint_handler.wait(p, stdin)
Josip Sokcevic6635baf2021-11-19 02:04:39 +0000217 stdout = stdout.decode('utf-8', 'ignore')
Edward Lemurecc27072020-01-06 16:42:34 +0000218 if timer.completed:
219 stdout = 'Process timed out after %ss\n%s' % (self.timeout, stdout)
Josip Sokcevic6635baf2021-11-19 02:04:39 +0000220 return p.returncode, stdout
Edward Lemurecc27072020-01-06 16:42:34 +0000221
222 def CallCommand(self, test):
223 """Runs an external program.
224
Edward Lemura5799e32020-01-17 19:26:51 +0000225 This function converts invocation of .py files and invocations of 'python'
Edward Lemurecc27072020-01-06 16:42:34 +0000226 to vpython invocations.
227 """
228 cmd = self._GetCommand(test)
Edward Lesmes8e282792018-04-03 18:50:29 -0400229 try:
Edward Lemurecc27072020-01-06 16:42:34 +0000230 start = time_time()
231 returncode, stdout = self._RunWithTimeout(cmd, test.stdin, test.kwargs)
232 duration = time_time() - start
Edward Lemur7cf94382019-11-15 22:36:41 +0000233 except Exception:
Edward Lemurecc27072020-01-06 16:42:34 +0000234 duration = time_time() - start
Edward Lesmes8e282792018-04-03 18:50:29 -0400235 return test.message(
Edward Lemur7cf94382019-11-15 22:36:41 +0000236 '%s\n%s exec failure (%4.2fs)\n%s' % (
237 test.name, ' '.join(cmd), duration, traceback.format_exc()))
Edward Lemurde9e3ca2019-10-24 21:13:31 +0000238
Edward Lemurecc27072020-01-06 16:42:34 +0000239 if returncode != 0:
Edward Lesmes8e282792018-04-03 18:50:29 -0400240 return test.message(
Edward Lemur7cf94382019-11-15 22:36:41 +0000241 '%s\n%s (%4.2fs) failed\n%s' % (
242 test.name, ' '.join(cmd), duration, stdout))
Edward Lemurecc27072020-01-06 16:42:34 +0000243
Edward Lesmes8e282792018-04-03 18:50:29 -0400244 if test.info:
Edward Lemur7cf94382019-11-15 22:36:41 +0000245 return test.info('%s\n%s (%4.2fs)' % (test.name, ' '.join(cmd), duration))
Edward Lesmes8e282792018-04-03 18:50:29 -0400246
247 def AddTests(self, tests, parallel=True):
248 if parallel:
249 self._tests.extend(tests)
250 else:
251 self._nonparallel_tests.extend(tests)
252
253 def RunAsync(self):
254 self._messages = []
255
256 def _WorkerFn():
257 while True:
258 test = None
259 with self._tests_lock:
260 if not self._tests:
261 break
262 test = self._tests.pop()
263 result = self.CallCommand(test)
264 if result:
265 with self._messages_lock:
266 self._messages.append(result)
267
268 def _StartDaemon():
269 t = threading.Thread(target=_WorkerFn)
270 t.daemon = True
271 t.start()
272 return t
273
274 while self._nonparallel_tests:
275 test = self._nonparallel_tests.pop()
276 result = self.CallCommand(test)
277 if result:
278 self._messages.append(result)
279
280 if self._tests:
281 threads = [_StartDaemon() for _ in range(self._pool_size)]
282 for worker in threads:
283 worker.join()
284
285 return self._messages
286
287
Josip Sokcevica9a7eec2023-03-10 03:54:52 +0000288def normpath(path):
289 '''Version of os.path.normpath that also changes backward slashes to
290 forward slashes when not running on Windows.
291 '''
292 # This is safe to always do because the Windows version of os.path.normpath
293 # will replace forward slashes with backward slashes.
294 path = path.replace(os.sep, '/')
295 return os.path.normpath(path)
296
297
Josip Sokcevic7958e302023-03-01 23:02:21 +0000298def _RightHandSideLinesImpl(affected_files):
299 """Implements RightHandSideLines for InputApi and GclChange."""
300 for af in affected_files:
301 lines = af.ChangedContents()
302 for line in lines:
303 yield (af, line[0], line[1])
304
305
Edward Lemur6eb1d322020-02-27 22:20:15 +0000306def prompt_should_continue(prompt_string):
307 sys.stdout.write(prompt_string)
Dirk Pranke7288f882021-06-03 18:01:30 +0000308 sys.stdout.flush()
Edward Lemur6eb1d322020-02-27 22:20:15 +0000309 response = sys.stdin.readline().strip().lower()
310 return response in ('y', 'yes')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000311
Josip Sokcevic48d8e902023-03-09 02:38:26 +0000312
313def _ShouldRunPresubmit(script_text, use_python3):
314 """Try to figure out whether these presubmit checks should be run under
315 python2 or python3. We need to do this without actually trying to
316 compile the text, since the text might compile in one but not the
317 other.
318
319 Args:
320 script_text: The text of the presubmit script.
321 use_python3: if true, will use python3 instead of python2 by default
322 if USE_PYTHON3 is not specified.
323
324 Return:
325 A boolean if presubmit should be executed
326 """
327 if os.getenv('LUCI_OMIT_PYTHON2') == 'true':
328 # If LUCI omits python2, run all presubmits with python3, regardless of
329 # USE_PYTHON3 variable.
330 return True
331
332 m = re.search('^USE_PYTHON3 = (True|False)$', script_text, flags=re.MULTILINE)
333 if m:
334 use_python3 = m.group(1) == 'True'
335
336 return ((sys.version_info.major == 2) and not use_python3) or \
337 ((sys.version_info.major == 3) and use_python3)
338
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000339# Top level object so multiprocessing can pickle
340# Public access through OutputApi object.
341class _PresubmitResult(object):
342 """Base class for result objects."""
343 fatal = False
344 should_prompt = False
345
346 def __init__(self, message, items=None, long_text=''):
347 """
348 message: A short one-line message to indicate errors.
349 items: A list of short strings to indicate where errors occurred.
350 long_text: multi-line text output, e.g. from another tool
351 """
Bruce Dawsondb8622b2022-04-03 15:38:12 +0000352 self._message = _PresubmitResult._ensure_str(message)
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000353 self._items = items or []
Tom McKee61c72652021-07-20 11:56:32 +0000354 self._long_text = _PresubmitResult._ensure_str(long_text.rstrip())
Bruce Dawsondca14bc2022-09-15 20:59:38 +0000355 if _SHOW_CALLSTACKS:
356 self._long_text += 'Presubmit result call stack is:\n'
357 self._long_text += ''.join(traceback.format_stack(None, 8))
Tom McKee61c72652021-07-20 11:56:32 +0000358
359 @staticmethod
360 def _ensure_str(val):
361 """
362 val: A "stringish" value. Can be any of str, unicode or bytes.
363 returns: A str after applying encoding/decoding as needed.
364 Assumes/uses UTF-8 for relevant inputs/outputs.
365
366 We'd prefer to use six.ensure_str but our copy of six is old :(
367 """
368 if isinstance(val, str):
369 return val
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000370
Tom McKee61c72652021-07-20 11:56:32 +0000371 if six.PY2 and isinstance(val, unicode):
372 return val.encode()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000373
374 if six.PY3 and isinstance(val, bytes):
Tom McKee61c72652021-07-20 11:56:32 +0000375 return val.decode()
376 raise ValueError("Unknown string type %s" % type(val))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000377
Edward Lemur6eb1d322020-02-27 22:20:15 +0000378 def handle(self):
379 sys.stdout.write(self._message)
380 sys.stdout.write('\n')
Takuto Ikutabaa7be02022-08-23 00:19:34 +0000381 for item in self._items:
Edward Lemur6eb1d322020-02-27 22:20:15 +0000382 sys.stdout.write(' ')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000383 # Write separately in case it's unicode.
Edward Lemur6eb1d322020-02-27 22:20:15 +0000384 sys.stdout.write(str(item))
Edward Lemur6eb1d322020-02-27 22:20:15 +0000385 sys.stdout.write('\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000386 if self._long_text:
Edward Lemur6eb1d322020-02-27 22:20:15 +0000387 sys.stdout.write('\n***************\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000388 # Write separately in case it's unicode.
Tom McKee61c72652021-07-20 11:56:32 +0000389 sys.stdout.write(self._long_text)
Edward Lemur6eb1d322020-02-27 22:20:15 +0000390 sys.stdout.write('\n***************\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000391
Debrian Figueroadd2737e2019-06-21 23:50:13 +0000392 def json_format(self):
393 return {
394 'message': self._message,
Debrian Figueroa6095d402019-06-28 18:47:18 +0000395 'items': [str(item) for item in self._items],
Debrian Figueroadd2737e2019-06-21 23:50:13 +0000396 'long_text': self._long_text,
397 'fatal': self.fatal
398 }
399
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000400
401# Top level object so multiprocessing can pickle
402# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000403class _PresubmitError(_PresubmitResult):
404 """A hard presubmit error."""
405 fatal = True
406
407
408# Top level object so multiprocessing can pickle
409# Public access through OutputApi object.
410class _PresubmitPromptWarning(_PresubmitResult):
411 """An warning that prompts the user if they want to continue."""
412 should_prompt = True
413
414
415# Top level object so multiprocessing can pickle
416# Public access through OutputApi object.
417class _PresubmitNotifyResult(_PresubmitResult):
418 """Just print something to the screen -- but it's not even a warning."""
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000419
420
421# Top level object so multiprocessing can pickle
422# Public access through OutputApi object.
423class _MailTextResult(_PresubmitResult):
424 """A warning that should be included in the review request email."""
425 def __init__(self, *args, **kwargs):
426 super(_MailTextResult, self).__init__()
427 raise NotImplementedError()
428
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000429class GerritAccessor(object):
430 """Limited Gerrit functionality for canned presubmit checks to work.
431
432 To avoid excessive Gerrit calls, caches the results.
433 """
434
Edward Lesmeseb1bd622021-03-01 19:54:07 +0000435 def __init__(self, url=None, project=None, branch=None):
436 self.host = urlparse.urlparse(url).netloc if url else None
437 self.project = project
438 self.branch = branch
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000439 self.cache = {}
Edward Lesmes8170c292021-03-19 20:04:43 +0000440 self.code_owners_enabled = None
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000441
442 def _FetchChangeDetail(self, issue):
443 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100444 try:
445 return gerrit_util.GetChangeDetail(
446 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700447 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100448 except gerrit_util.GerritError as e:
449 if e.http_status == 404:
450 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
451 'no credentials to fetch issue details' % issue)
452 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000453
454 def GetChangeInfo(self, issue):
455 """Returns labels and all revisions (patchsets) for this issue.
456
457 The result is a dictionary according to Gerrit REST Api.
458 https://gerrit-review.googlesource.com/Documentation/rest-api.html
459
460 However, API isn't very clear what's inside, so see tests for example.
461 """
462 assert issue
463 cache_key = int(issue)
464 if cache_key not in self.cache:
465 self.cache[cache_key] = self._FetchChangeDetail(issue)
466 return self.cache[cache_key]
467
468 def GetChangeDescription(self, issue, patchset=None):
469 """If patchset is none, fetches current patchset."""
470 info = self.GetChangeInfo(issue)
471 # info is a reference to cache. We'll modify it here adding description to
472 # it to the right patchset, if it is not yet there.
473
474 # Find revision info for the patchset we want.
475 if patchset is not None:
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000476 for rev, rev_info in info['revisions'].items():
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000477 if str(rev_info['_number']) == str(patchset):
478 break
479 else:
480 raise Exception('patchset %s doesn\'t exist in issue %s' % (
481 patchset, issue))
482 else:
483 rev = info['current_revision']
484 rev_info = info['revisions'][rev]
485
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100486 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000487
Mun Yong Jang603d01e2017-12-19 16:38:30 -0800488 def GetDestRef(self, issue):
489 ref = self.GetChangeInfo(issue)['branch']
490 if not ref.startswith('refs/'):
491 # NOTE: it is possible to create 'refs/x' branch,
492 # aka 'refs/heads/refs/x'. However, this is ill-advised.
493 ref = 'refs/heads/%s' % ref
494 return ref
495
Edward Lesmes02d4b822020-11-11 00:37:35 +0000496 def _GetApproversForLabel(self, issue, label):
497 change_info = self.GetChangeInfo(issue)
498 label_info = change_info.get('labels', {}).get(label, {})
499 values = label_info.get('values', {}).keys()
500 if not values:
501 return []
502 max_value = max(int(v) for v in values)
503 return [v for v in label_info.get('all', [])
504 if v.get('value', 0) == max_value]
505
Edward Lesmesc4566172021-03-19 16:55:13 +0000506 def IsBotCommitApproved(self, issue):
507 return bool(self._GetApproversForLabel(issue, 'Bot-Commit'))
508
Edward Lesmescf49cb82020-11-11 01:08:36 +0000509 def IsOwnersOverrideApproved(self, issue):
510 return bool(self._GetApproversForLabel(issue, 'Owners-Override'))
511
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000512 def GetChangeOwner(self, issue):
513 return self.GetChangeInfo(issue)['owner']['email']
514
515 def GetChangeReviewers(self, issue, approving_only=True):
Aaron Gable8b478f02017-07-31 15:33:19 -0700516 changeinfo = self.GetChangeInfo(issue)
517 if approving_only:
Edward Lesmes02d4b822020-11-11 00:37:35 +0000518 reviewers = self._GetApproversForLabel(issue, 'Code-Review')
Aaron Gable8b478f02017-07-31 15:33:19 -0700519 else:
520 reviewers = changeinfo.get('reviewers', {}).get('REVIEWER', [])
521 return [r.get('email') for r in reviewers]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000522
Edward Lemure4d329c2020-02-03 20:41:18 +0000523 def UpdateDescription(self, description, issue):
524 gerrit_util.SetCommitMessage(self.host, issue, description, notify='NONE')
525
Edward Lesmes8170c292021-03-19 20:04:43 +0000526 def IsCodeOwnersEnabledOnRepo(self):
527 if self.code_owners_enabled is None:
528 self.code_owners_enabled = gerrit_util.IsCodeOwnersEnabledOnRepo(
Edward Lesmes392c4072021-03-19 21:58:45 +0000529 self.host, self.project)
Edward Lesmes8170c292021-03-19 20:04:43 +0000530 return self.code_owners_enabled
531
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000532
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000533class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000534 """An instance of OutputApi gets passed to presubmit scripts so that they
535 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000536 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000537 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000538 PresubmitError = _PresubmitError
539 PresubmitPromptWarning = _PresubmitPromptWarning
540 PresubmitNotifyResult = _PresubmitNotifyResult
541 MailTextResult = _MailTextResult
542
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000543 def __init__(self, is_committing):
544 self.is_committing = is_committing
Daniel Cheng7227d212017-11-17 08:12:37 -0800545 self.more_cc = []
546
547 def AppendCC(self, cc):
548 """Appends a user to cc for this change."""
Daniel Cheng0e9f6682022-10-19 17:42:57 +0000549 if cc not in self.more_cc:
550 self.more_cc.append(cc)
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000551
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000552 def PresubmitPromptOrNotify(self, *args, **kwargs):
553 """Warn the user when uploading, but only notify if committing."""
554 if self.is_committing:
555 return self.PresubmitNotifyResult(*args, **kwargs)
556 return self.PresubmitPromptWarning(*args, **kwargs)
557
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000558
559class InputApi(object):
560 """An instance of this object is passed to presubmit scripts so they can
561 know stuff about the change they're looking at.
562 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000563 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800564 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000565
maruel@chromium.org3410d912009-06-09 20:56:16 +0000566 # File extensions that are considered source files from a style guide
567 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000568 #
569 # Files without an extension aren't included in the list. If you want to
local_bot30f774e2020-06-25 18:23:34 +0000570 # filter them as source files, add r'(^|.*?[\\\/])[^.]+$' to the allow list.
local_bot64021412020-07-08 21:05:39 +0000571 # Note that ALL CAPS files are skipped in DEFAULT_FILES_TO_SKIP below.
572 DEFAULT_FILES_TO_CHECK = (
maruel@chromium.org3410d912009-06-09 20:56:16 +0000573 # C++ and friends
Edward Lemura5799e32020-01-17 19:26:51 +0000574 r'.+\.c$', r'.+\.cc$', r'.+\.cpp$', r'.+\.h$', r'.+\.m$', r'.+\.mm$',
575 r'.+\.inl$', r'.+\.asm$', r'.+\.hxx$', r'.+\.hpp$', r'.+\.s$', r'.+\.S$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000576 # Scripts
dpapad443d9132022-05-05 00:17:30 +0000577 r'.+\.js$', r'.+\.ts$', r'.+\.py$', r'.+\.sh$', r'.+\.rb$', r'.+\.pl$',
578 r'.+\.pm$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000579 # Other
Edward Lemura5799e32020-01-17 19:26:51 +0000580 r'.+\.java$', r'.+\.mk$', r'.+\.am$', r'.+\.css$', r'.+\.mojom$',
Bruce Dawson7a81ebf2023-01-03 18:36:18 +0000581 r'.+\.fidl$', r'.+\.rs$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000582 )
583
584 # Path regexp that should be excluded from being considered containing source
585 # files. Don't modify this list from a presubmit script!
local_bot64021412020-07-08 21:05:39 +0000586 DEFAULT_FILES_TO_SKIP = (
Edward Lemura5799e32020-01-17 19:26:51 +0000587 r'testing_support[\\\/]google_appengine[\\\/].*',
588 r'.*\bexperimental[\\\/].*',
Kent Tamura179dd1e2018-04-26 15:07:41 +0900589 # Exclude third_party/.* but NOT third_party/{WebKit,blink}
590 # (crbug.com/539768 and crbug.com/836555).
Edward Lemura5799e32020-01-17 19:26:51 +0000591 r'.*\bthird_party[\\\/](?!(WebKit|blink)[\\\/]).*',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000592 # Output directories (just in case)
Edward Lemura5799e32020-01-17 19:26:51 +0000593 r'.*\bDebug[\\\/].*',
594 r'.*\bRelease[\\\/].*',
595 r'.*\bxcodebuild[\\\/].*',
596 r'.*\bout[\\\/].*',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000597 # All caps files like README and LICENCE.
Edward Lemura5799e32020-01-17 19:26:51 +0000598 r'.*\b[A-Z0-9_]{2,}$',
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000599 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
Edward Lemura5799e32020-01-17 19:26:51 +0000600 r'(|.*[\\\/])\.git[\\\/].*',
601 r'(|.*[\\\/])\.svn[\\\/].*',
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000602 # There is no point in processing a patch file.
Edward Lemura5799e32020-01-17 19:26:51 +0000603 r'.+\.diff$',
604 r'.+\.patch$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000605 )
606
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000607 def __init__(self, change, presubmit_path, is_committing,
Bruce Dawson09c0c072022-05-26 20:28:58 +0000608 verbose, gerrit_obj, dry_run=None, thread_pool=None, parallel=False,
609 no_diffs=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000610 """Builds an InputApi object.
611
612 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000613 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000614 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000615 is_committing: True if the change is about to be committed.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000616 gerrit_obj: provides basic Gerrit codereview functionality.
617 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -0400618 parallel: if true, all tests reported via input_api.RunTests for all
619 PRESUBMIT files will be run in parallel.
Bruce Dawson09c0c072022-05-26 20:28:58 +0000620 no_diffs: if true, implies that --files or --all was specified so some
621 checks can be skipped, and some errors will be messages.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000622 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000623 # Version number of the presubmit_support script.
624 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000625 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000626 self.is_committing = is_committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000627 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000628 self.dry_run = dry_run
Bruce Dawson09c0c072022-05-26 20:28:58 +0000629 self.no_diffs = no_diffs
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000630
Edward Lesmes8e282792018-04-03 18:50:29 -0400631 self.parallel = parallel
632 self.thread_pool = thread_pool or ThreadPool()
633
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000634 # We expose various modules and functions as attributes of the input_api
635 # so that presubmit scripts don't have to import them.
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +0900636 self.ast = ast
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000637 self.basename = os.path.basename
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000638 self.cpplint = cpplint
dcheng091b7db2016-06-16 01:27:51 -0700639 self.fnmatch = fnmatch
Yoshisato Yanagisawa04600b42019-03-15 03:03:41 +0000640 self.gclient_paths = gclient_paths
Yoshisato Yanagisawa57dd17b2019-03-22 09:10:29 +0000641 # TODO(yyanagisawa): stop exposing this when python3 become default.
642 # Since python3's tempfile has TemporaryDirectory, we do not need this.
643 self.temporary_directory = gclient_utils.temporary_directory
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000644 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000645 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000646 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000647 self.os_listdir = os.listdir
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000648 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000649 self.os_stat = os.stat
Yoshisato Yanagisawa406de132018-06-29 05:43:25 +0000650 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000651 self.re = re
652 self.subprocess = subprocess
Edward Lemurb9830242019-10-30 22:19:20 +0000653 self.sys = sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000654 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000655 self.time = time
maruel@chromium.org1487d532009-06-06 00:22:57 +0000656 self.unittest = unittest
Edward Lemurb9830242019-10-30 22:19:20 +0000657 if sys.version_info.major == 2:
658 self.urllib2 = urllib2
Edward Lemur16af3562019-10-17 22:11:33 +0000659 self.urllib_request = urllib_request
660 self.urllib_error = urllib_error
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000661
Robert Iannucci50258932018-03-19 10:30:59 -0700662 self.is_windows = sys.platform == 'win32'
663
Edward Lemurb9646622019-10-25 20:57:35 +0000664 # Set python_executable to 'vpython' in order to allow scripts in other
665 # repos (e.g. src.git) to automatically pick up that repo's .vpython file,
666 # instead of inheriting the one in depot_tools.
667 self.python_executable = 'vpython'
Erik Staab69135d12021-05-14 22:31:57 +0000668 # Offer a python 3 executable for use during the migration off of python 2.
669 self.python3_executable = 'vpython3'
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000670 self.environ = os.environ
671
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000672 # InputApi.platform is the platform you're currently running on.
673 self.platform = sys.platform
674
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000675 self.cpu_count = multiprocessing.cpu_count()
Bruce Dawson8254f062022-06-17 17:33:08 +0000676 if self.is_windows:
677 # TODO(crbug.com/1190269) - we can't use more than 56 child processes on
678 # Windows or Python3 may hang.
679 self.cpu_count = min(self.cpu_count, 56)
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000680
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000681 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000682 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683
684 # We carry the canned checks so presubmit scripts can easily use them.
685 self.canned_checks = presubmit_canned_checks
686
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100687 # Temporary files we must manually remove at the end of a run.
688 self._named_temporary_files = []
Jochen Eisinger72606f82017-04-04 10:44:18 +0200689
Edward Lesmesdc11c6b2021-03-19 19:22:05 +0000690 self.owners_client = None
Bruce Dawsoneb8426e2022-08-05 23:58:15 +0000691 if self.gerrit and not 'PRESUBMIT_SKIP_NETWORK' in self.environ:
Bruce Dawson9b9f4512022-04-27 00:56:52 +0000692 try:
693 self.owners_client = owners_client.GetCodeOwnersClient(
Bruce Dawson9b9f4512022-04-27 00:56:52 +0000694 host=self.gerrit.host,
695 project=self.gerrit.project,
696 branch=self.gerrit.branch)
697 except Exception as e:
698 print('Failed to set owners_client - %s' % str(e))
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200699 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000700 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000701 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000702
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000703 # Replace <hash_map> and <hash_set> as headers that need to be included
Edward Lemura5799e32020-01-17 19:26:51 +0000704 # with 'base/containers/hash_tables.h' instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000705 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800706 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000707 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000708 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000709 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
710 for (a, b, header) in cpplint._re_pattern_templates
711 ]
712
Edward Lemurecc27072020-01-06 16:42:34 +0000713 def SetTimeout(self, timeout):
714 self.thread_pool.timeout = timeout
715
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000716 def PresubmitLocalPath(self):
717 """Returns the local path of the presubmit script currently being run.
718
719 This is useful if you don't want to hard-code absolute paths in the
720 presubmit script. For example, It can be used to find another file
721 relative to the PRESUBMIT.py script, so the whole tree can be branched and
722 the presubmit script still works, without editing its content.
723 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000724 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000725
agable0b65e732016-11-22 09:25:46 -0800726 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000727 """Same as input_api.change.AffectedFiles() except only lists files
728 (and optionally directories) in the same directory as the current presubmit
Bruce Dawson7a0b07a2020-04-23 17:14:40 +0000729 script, or subdirectories thereof. Note that files are listed using the OS
730 path separator, so backslashes are used as separators on Windows.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000731 """
Josip Sokcevica9a7eec2023-03-10 03:54:52 +0000732 dir_with_slash = normpath(self.PresubmitLocalPath())
Bruce Dawson31bfd512022-05-10 23:19:39 +0000733 # normpath strips trailing path separators, so the trailing separator has to
734 # be added after the normpath call.
735 if len(dir_with_slash) > 0:
736 dir_with_slash += os.path.sep
sail@chromium.org5538e022011-05-12 17:53:16 +0000737
Josip Sokcevica9a7eec2023-03-10 03:54:52 +0000738 return list(filter(
739 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
740 self.change.AffectedFiles(include_deletes, file_filter)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000741
agable0b65e732016-11-22 09:25:46 -0800742 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000743 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800744 paths = [af.LocalPath() for af in self.AffectedFiles()]
Edward Lemura5799e32020-01-17 19:26:51 +0000745 logging.debug('LocalPaths: %s', paths)
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000746 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000747
agable0b65e732016-11-22 09:25:46 -0800748 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000749 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800750 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000751
John Budorick16162372018-04-18 10:39:53 -0700752 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
agable0b65e732016-11-22 09:25:46 -0800753 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000754 in the same directory as the current presubmit script, or subdirectories
755 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000756 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000757 if include_deletes is not None:
Edward Lemura5799e32020-01-17 19:26:51 +0000758 warn('AffectedTestableFiles(include_deletes=%s)'
759 ' is deprecated and ignored' % str(include_deletes),
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000760 category=DeprecationWarning,
761 stacklevel=2)
Josip Sokcevic4de5dea2022-03-23 21:15:14 +0000762 # pylint: disable=consider-using-generator
763 return [
764 x for x in self.AffectedFiles(include_deletes=False, **kwargs)
765 if x.IsTestableFile()
766 ]
agable0b65e732016-11-22 09:25:46 -0800767
768 def AffectedTextFiles(self, include_deletes=None):
769 """An alias to AffectedTestableFiles for backwards compatibility."""
770 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000771
Josip Sokcevic8c955952021-02-01 21:32:57 +0000772 def FilterSourceFile(self,
773 affected_file,
774 files_to_check=None,
775 files_to_skip=None,
776 allow_list=None,
777 block_list=None):
Edward Lemura5799e32020-01-17 19:26:51 +0000778 """Filters out files that aren't considered 'source file'.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000779
local_bot64021412020-07-08 21:05:39 +0000780 If files_to_check or files_to_skip is None, InputApi.DEFAULT_FILES_TO_CHECK
781 and InputApi.DEFAULT_FILES_TO_SKIP is used respectively.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000782
Bruce Dawson635383f2022-09-13 16:23:18 +0000783 affected_file.LocalPath() needs to re.match an entry in the files_to_check
784 list and not re.match any entries in the files_to_skip list.
785 '/' path separators should be used in the regular expressions and will work
786 on Windows as well as other platforms.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000787
788 Note: Copy-paste this function to suit your needs or use a lambda function.
789 """
local_bot64021412020-07-08 21:05:39 +0000790 if files_to_check is None:
791 files_to_check = self.DEFAULT_FILES_TO_CHECK
792 if files_to_skip is None:
793 files_to_skip = self.DEFAULT_FILES_TO_SKIP
local_bot30f774e2020-06-25 18:23:34 +0000794
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000795 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000796 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000797 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000798 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000799 return True
Bruce Dawsona3a014e2022-04-27 23:28:17 +0000800 # Handle the cases where the files regex only handles /, but the local
801 # path uses \.
802 if self.is_windows and self.re.match(item, local_path.replace(
803 '\\', '/')):
804 return True
maruel@chromium.org3410d912009-06-09 20:56:16 +0000805 return False
local_bot64021412020-07-08 21:05:39 +0000806 return (Find(affected_file, files_to_check) and
807 not Find(affected_file, files_to_skip))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000808
809 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800810 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000811
812 If source_file is None, InputApi.FilterSourceFile() is used.
813 """
814 if not source_file:
815 source_file = self.FilterSourceFile
Edward Lemurb9830242019-10-30 22:19:20 +0000816 return list(filter(source_file, self.AffectedTestableFiles()))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000817
818 def RightHandSideLines(self, source_file_filter=None):
Edward Lemura5799e32020-01-17 19:26:51 +0000819 """An iterator over all text lines in 'new' version of changed files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000820
821 Only lists lines from new or modified text files in the change that are
822 contained by the directory of the currently executing presubmit script.
823
824 This is useful for doing line-by-line regex checks, like checking for
825 trailing whitespace.
826
827 Yields:
828 a 3 tuple:
Josip Sokcevic7958e302023-03-01 23:02:21 +0000829 the AffectedFile instance of the current file;
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000830 integer line number (1-based); and
831 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000832
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000833 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000834 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000835 files = self.AffectedSourceFiles(source_file_filter)
Josip Sokcevic7958e302023-03-01 23:02:21 +0000836 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000837
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000838 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000839 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000840
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000841 Deny reading anything outside the repository.
842 """
Josip Sokcevic7958e302023-03-01 23:02:21 +0000843 if isinstance(file_item, AffectedFile):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000844 file_item = file_item.AbsoluteLocalPath()
845 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000846 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000847 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000848
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100849 def CreateTemporaryFile(self, **kwargs):
850 """Returns a named temporary file that must be removed with a call to
851 RemoveTemporaryFiles().
852
853 All keyword arguments are forwarded to tempfile.NamedTemporaryFile(),
854 except for |delete|, which is always set to False.
855
856 Presubmit checks that need to create a temporary file and pass it for
857 reading should use this function instead of NamedTemporaryFile(), as
858 Windows fails to open a file that is already open for writing.
859
860 with input_api.CreateTemporaryFile() as f:
861 f.write('xyz')
862 f.close()
863 input_api.subprocess.check_output(['script-that', '--reads-from',
864 f.name])
865
866
867 Note that callers of CreateTemporaryFile() should not worry about removing
868 any temporary file; this is done transparently by the presubmit handling
869 code.
870 """
871 if 'delete' in kwargs:
872 # Prevent users from passing |delete|; we take care of file deletion
873 # ourselves and this prevents unintuitive error messages when we pass
874 # delete=False and 'delete' is also in kwargs.
875 raise TypeError('CreateTemporaryFile() does not take a "delete" '
876 'argument, file deletion is handled automatically by '
877 'the same presubmit_support code that creates InputApi '
878 'objects.')
879 temp_file = self.tempfile.NamedTemporaryFile(delete=False, **kwargs)
880 self._named_temporary_files.append(temp_file.name)
881 return temp_file
882
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000883 @property
884 def tbr(self):
885 """Returns if a change is TBR'ed."""
Jeremy Romandce22502017-06-20 15:37:29 -0400886 return 'TBR' in self.change.tags or self.change.TBRsFromDescription()
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000887
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000888 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000889 tests = []
890 msgs = []
891 for t in tests_mix:
Edward Lesmes8e282792018-04-03 18:50:29 -0400892 if isinstance(t, OutputApi.PresubmitResult) and t:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000893 msgs.append(t)
894 else:
895 assert issubclass(t.message, _PresubmitResult)
896 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000897 if self.verbose:
898 t.info = _PresubmitNotifyResult
Edward Lemur1037c742018-05-01 18:56:04 -0400899 if not t.kwargs.get('cwd'):
900 t.kwargs['cwd'] = self.PresubmitLocalPath()
Edward Lesmes8e282792018-04-03 18:50:29 -0400901 self.thread_pool.AddTests(tests, parallel)
Edward Lemur21000eb2019-05-24 23:25:58 +0000902 # When self.parallel is True (i.e. --parallel is passed as an option)
903 # RunTests doesn't actually run tests. It adds them to a ThreadPool that
904 # will run all tests once all PRESUBMIT files are processed.
905 # Otherwise, it will run them and return the results.
906 if not self.parallel:
Edward Lesmes8e282792018-04-03 18:50:29 -0400907 msgs.extend(self.thread_pool.RunAsync())
908 return msgs
scottmg86099d72016-09-01 09:16:51 -0700909
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000910
Josip Sokcevic7958e302023-03-01 23:02:21 +0000911class _DiffCache(object):
912 """Caches diffs retrieved from a particular SCM."""
913 def __init__(self, upstream=None):
914 """Stores the upstream revision against which all diffs will be computed."""
915 self._upstream = upstream
916
917 def GetDiff(self, path, local_root):
918 """Get the diff for a particular path."""
919 raise NotImplementedError()
920
921 def GetOldContents(self, path, local_root):
922 """Get the old version for a particular path."""
923 raise NotImplementedError()
924
925
926class _GitDiffCache(_DiffCache):
927 """DiffCache implementation for git; gets all file diffs at once."""
928 def __init__(self, upstream):
929 super(_GitDiffCache, self).__init__(upstream=upstream)
930 self._diffs_by_file = None
931
932 def GetDiff(self, path, local_root):
933 # Compare against None to distinguish between None and an initialized but
934 # empty dictionary.
935 if self._diffs_by_file == None:
936 # Compute a single diff for all files and parse the output; should
937 # with git this is much faster than computing one diff for each file.
938 diffs = {}
939
940 # Don't specify any filenames below, because there are command line length
941 # limits on some platforms and GenerateDiff would fail.
942 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
943 branch=self._upstream)
944
945 # This regex matches the path twice, separated by a space. Note that
946 # filename itself may contain spaces.
947 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
948 current_diff = []
949 keep_line_endings = True
950 for x in unified_diff.splitlines(keep_line_endings):
951 match = file_marker.match(x)
952 if match:
953 # Marks the start of a new per-file section.
954 diffs[match.group('filename')] = current_diff = [x]
955 elif x.startswith('diff --git'):
956 raise PresubmitFailure('Unexpected diff line: %s' % x)
957 else:
958 current_diff.append(x)
959
960 self._diffs_by_file = dict(
Josip Sokcevica9a7eec2023-03-10 03:54:52 +0000961 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
Josip Sokcevic7958e302023-03-01 23:02:21 +0000962
963 if path not in self._diffs_by_file:
964 # SCM didn't have any diff on this file. It could be that the file was not
965 # modified at all (e.g. user used --all flag in git cl presubmit).
966 # Intead of failing, return empty string.
967 # See: https://crbug.com/808346.
968 return ''
969
970 return self._diffs_by_file[path]
971
972 def GetOldContents(self, path, local_root):
973 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
974
975
976class AffectedFile(object):
977 """Representation of a file in a change."""
978
979 DIFF_CACHE = _DiffCache
980
981 # Method could be a function
982 # pylint: disable=no-self-use
983 def __init__(self, path, action, repository_root, diff_cache):
984 self._path = path
985 self._action = action
986 self._local_root = repository_root
987 self._is_directory = None
988 self._cached_changed_contents = None
989 self._cached_new_contents = None
990 self._diff_cache = diff_cache
991 logging.debug('%s(%s)', self.__class__.__name__, self._path)
992
993 def LocalPath(self):
994 """Returns the path of this file on the local disk relative to client root.
995
996 This should be used for error messages but not for accessing files,
997 because presubmit checks are run with CWD=PresubmitLocalPath() (which is
998 often != client root).
999 """
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001000 return normpath(self._path)
Josip Sokcevic7958e302023-03-01 23:02:21 +00001001
1002 def AbsoluteLocalPath(self):
1003 """Returns the absolute path of this file on the local disk.
1004 """
1005 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
1006
1007 def Action(self):
1008 """Returns the action on this opened file, e.g. A, M, D, etc."""
1009 return self._action
1010
1011 def IsTestableFile(self):
1012 """Returns True if the file is a text file and not a binary file.
1013
1014 Deleted files are not text file."""
1015 raise NotImplementedError() # Implement when needed
1016
1017 def IsTextFile(self):
1018 """An alias to IsTestableFile for backwards compatibility."""
1019 return self.IsTestableFile()
1020
1021 def OldContents(self):
1022 """Returns an iterator over the lines in the old version of file.
1023
1024 The old version is the file before any modifications in the user's
1025 workspace, i.e. the 'left hand side'.
1026
1027 Contents will be empty if the file is a directory or does not exist.
1028 Note: The carriage returns (LF or CR) are stripped off.
1029 """
1030 return self._diff_cache.GetOldContents(self.LocalPath(),
1031 self._local_root).splitlines()
1032
1033 def NewContents(self):
1034 """Returns an iterator over the lines in the new version of file.
1035
1036 The new version is the file in the user's workspace, i.e. the 'right hand
1037 side'.
1038
1039 Contents will be empty if the file is a directory or does not exist.
1040 Note: The carriage returns (LF or CR) are stripped off.
1041 """
1042 if self._cached_new_contents is None:
1043 self._cached_new_contents = []
1044 try:
1045 self._cached_new_contents = gclient_utils.FileRead(
1046 self.AbsoluteLocalPath(), 'rU').splitlines()
1047 except IOError:
1048 pass # File not found? That's fine; maybe it was deleted.
1049 except UnicodeDecodeError as e:
1050 # log the filename since we're probably trying to read a binary
1051 # file, and shouldn't be.
1052 print('Error reading %s: %s' % (self.AbsoluteLocalPath(), e))
1053 raise
1054
1055 return self._cached_new_contents[:]
1056
1057 def ChangedContents(self, keeplinebreaks=False):
1058 """Returns a list of tuples (line number, line text) of all new lines.
1059
1060 This relies on the scm diff output describing each changed code section
1061 with a line of the form
1062
1063 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
1064 """
1065 # Don't return cached results when line breaks are requested.
1066 if not keeplinebreaks and self._cached_changed_contents is not None:
1067 return self._cached_changed_contents[:]
1068 result = []
1069 line_num = 0
1070
1071 # The keeplinebreaks parameter to splitlines must be True or else the
1072 # CheckForWindowsLineEndings presubmit will be a NOP.
1073 for line in self.GenerateScmDiff().splitlines(keeplinebreaks):
1074 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
1075 if m:
1076 line_num = int(m.groups(1)[0])
1077 continue
1078 if line.startswith('+') and not line.startswith('++'):
1079 result.append((line_num, line[1:]))
1080 if not line.startswith('-'):
1081 line_num += 1
1082 # Don't cache results with line breaks.
1083 if keeplinebreaks:
1084 return result;
1085 self._cached_changed_contents = result
1086 return self._cached_changed_contents[:]
1087
1088 def __str__(self):
1089 return self.LocalPath()
1090
1091 def GenerateScmDiff(self):
1092 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
1093
1094
1095class GitAffectedFile(AffectedFile):
1096 """Representation of a file in a change out of a git checkout."""
1097 # Method 'NNN' is abstract in class 'NNN' but is not overridden
1098 # pylint: disable=abstract-method
1099
1100 DIFF_CACHE = _GitDiffCache
1101
1102 def __init__(self, *args, **kwargs):
1103 AffectedFile.__init__(self, *args, **kwargs)
1104 self._server_path = None
1105 self._is_testable_file = None
1106
1107 def IsTestableFile(self):
1108 if self._is_testable_file is None:
1109 if self.Action() == 'D':
1110 # A deleted file is not testable.
1111 self._is_testable_file = False
1112 else:
1113 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
1114 return self._is_testable_file
1115
1116
1117class Change(object):
1118 """Describe a change.
1119
1120 Used directly by the presubmit scripts to query the current change being
1121 tested.
1122
1123 Instance members:
1124 tags: Dictionary of KEY=VALUE pairs found in the change description.
1125 self.KEY: equivalent to tags['KEY']
1126 """
1127
1128 _AFFECTED_FILES = AffectedFile
1129
1130 # Matches key/value (or 'tag') lines in changelist descriptions.
1131 TAG_LINE_RE = re.compile(
1132 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
1133 scm = ''
1134
1135 def __init__(
1136 self, name, description, local_root, files, issue, patchset, author,
1137 upstream=None):
1138 if files is None:
1139 files = []
1140 self._name = name
1141 # Convert root into an absolute path.
1142 self._local_root = os.path.abspath(local_root)
1143 self._upstream = upstream
1144 self.issue = issue
1145 self.patchset = patchset
1146 self.author_email = author
1147
1148 self._full_description = ''
1149 self.tags = {}
1150 self._description_without_tags = ''
1151 self.SetDescriptionText(description)
1152
1153 assert all(
1154 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
1155
1156 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
1157 self._affected_files = [
1158 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
1159 for action, path in files
1160 ]
1161
1162 def UpstreamBranch(self):
1163 """Returns the upstream branch for the change."""
1164 return self._upstream
1165
1166 def Name(self):
1167 """Returns the change name."""
1168 return self._name
1169
1170 def DescriptionText(self):
1171 """Returns the user-entered changelist description, minus tags.
1172
1173 Any line in the user-provided description starting with e.g. 'FOO='
1174 (whitespace permitted before and around) is considered a tag line. Such
1175 lines are stripped out of the description this function returns.
1176 """
1177 return self._description_without_tags
1178
1179 def FullDescriptionText(self):
1180 """Returns the complete changelist description including tags."""
1181 return self._full_description
1182
1183 def SetDescriptionText(self, description):
1184 """Sets the full description text (including tags) to |description|.
1185
1186 Also updates the list of tags."""
1187 self._full_description = description
1188
1189 # From the description text, build up a dictionary of key/value pairs
1190 # plus the description minus all key/value or 'tag' lines.
1191 description_without_tags = []
1192 self.tags = {}
1193 for line in self._full_description.splitlines():
1194 m = self.TAG_LINE_RE.match(line)
1195 if m:
1196 self.tags[m.group('key')] = m.group('value')
1197 else:
1198 description_without_tags.append(line)
1199
1200 # Change back to text and remove whitespace at end.
1201 self._description_without_tags = (
1202 '\n'.join(description_without_tags).rstrip())
1203
1204 def AddDescriptionFooter(self, key, value):
1205 """Adds the given footer to the change description.
1206
1207 Args:
1208 key: A string with the key for the git footer. It must conform to
1209 the git footers format (i.e. 'List-Of-Tokens') and will be case
1210 normalized so that each token is title-cased.
1211 value: A string with the value for the git footer.
1212 """
1213 description = git_footers.add_footer(
1214 self.FullDescriptionText(), git_footers.normalize_name(key), value)
1215 self.SetDescriptionText(description)
1216
1217 def RepositoryRoot(self):
1218 """Returns the repository (checkout) root directory for this change,
1219 as an absolute path.
1220 """
1221 return self._local_root
1222
1223 def __getattr__(self, attr):
1224 """Return tags directly as attributes on the object."""
1225 if not re.match(r'^[A-Z_]*$', attr):
1226 raise AttributeError(self, attr)
1227 return self.tags.get(attr)
1228
1229 def GitFootersFromDescription(self):
1230 """Return the git footers present in the description.
1231
1232 Returns:
1233 footers: A dict of {footer: [values]} containing a multimap of the footers
1234 in the change description.
1235 """
1236 return git_footers.parse_footers(self.FullDescriptionText())
1237
1238 def BugsFromDescription(self):
1239 """Returns all bugs referenced in the commit description."""
1240 bug_tags = ['BUG', 'FIXED']
1241
1242 tags = []
1243 for tag in bug_tags:
1244 values = self.tags.get(tag)
1245 if values:
1246 tags += [value.strip() for value in values.split(',')]
1247
1248 footers = []
1249 parsed = self.GitFootersFromDescription()
1250 unsplit_footers = parsed.get('Bug', []) + parsed.get('Fixed', [])
1251 for unsplit_footer in unsplit_footers:
1252 footers += [b.strip() for b in unsplit_footer.split(',')]
1253 return sorted(set(tags + footers))
1254
1255 def ReviewersFromDescription(self):
1256 """Returns all reviewers listed in the commit description."""
1257 # We don't support a 'R:' git-footer for reviewers; that is in metadata.
1258 tags = [r.strip() for r in self.tags.get('R', '').split(',') if r.strip()]
1259 return sorted(set(tags))
1260
1261 def TBRsFromDescription(self):
1262 """Returns all TBR reviewers listed in the commit description."""
1263 tags = [r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()]
1264 # TODO(crbug.com/839208): Remove support for 'Tbr:' when TBRs are
1265 # programmatically determined by self-CR+1s.
1266 footers = self.GitFootersFromDescription().get('Tbr', [])
1267 return sorted(set(tags + footers))
1268
1269 # TODO(crbug.com/753425): Delete these once we're sure they're unused.
1270 @property
1271 def BUG(self):
1272 return ','.join(self.BugsFromDescription())
1273 @property
1274 def R(self):
1275 return ','.join(self.ReviewersFromDescription())
1276 @property
1277 def TBR(self):
1278 return ','.join(self.TBRsFromDescription())
1279
1280 def AllFiles(self, root=None):
1281 """List all files under source control in the repo."""
1282 raise NotImplementedError()
1283
1284 def AffectedFiles(self, include_deletes=True, file_filter=None):
1285 """Returns a list of AffectedFile instances for all files in the change.
1286
1287 Args:
1288 include_deletes: If false, deleted files will be filtered out.
1289 file_filter: An additional filter to apply.
1290
1291 Returns:
1292 [AffectedFile(path, action), AffectedFile(path, action)]
1293 """
1294 affected = list(filter(file_filter, self._affected_files))
1295
1296 if include_deletes:
1297 return affected
1298 return list(filter(lambda x: x.Action() != 'D', affected))
1299
1300 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
1301 """Return a list of the existing text files in a change."""
1302 if include_deletes is not None:
1303 warn('AffectedTeestableFiles(include_deletes=%s)'
1304 ' is deprecated and ignored' % str(include_deletes),
1305 category=DeprecationWarning,
1306 stacklevel=2)
1307 return list(filter(
1308 lambda x: x.IsTestableFile(),
1309 self.AffectedFiles(include_deletes=False, **kwargs)))
1310
1311 def AffectedTextFiles(self, include_deletes=None):
1312 """An alias to AffectedTestableFiles for backwards compatibility."""
1313 return self.AffectedTestableFiles(include_deletes=include_deletes)
1314
1315 def LocalPaths(self):
1316 """Convenience function."""
1317 return [af.LocalPath() for af in self.AffectedFiles()]
1318
1319 def AbsoluteLocalPaths(self):
1320 """Convenience function."""
1321 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
1322
1323 def RightHandSideLines(self):
1324 """An iterator over all text lines in 'new' version of changed files.
1325
1326 Lists lines from new or modified text files in the change.
1327
1328 This is useful for doing line-by-line regex checks, like checking for
1329 trailing whitespace.
1330
1331 Yields:
1332 a 3 tuple:
1333 the AffectedFile instance of the current file;
1334 integer line number (1-based); and
1335 the contents of the line as a string.
1336 """
1337 return _RightHandSideLinesImpl(
1338 x for x in self.AffectedFiles(include_deletes=False)
1339 if x.IsTestableFile())
1340
1341 def OriginalOwnersFiles(self):
1342 """A map from path names of affected OWNERS files to their old content."""
1343 def owners_file_filter(f):
1344 return 'OWNERS' in os.path.split(f.LocalPath())[1]
1345 files = self.AffectedFiles(file_filter=owners_file_filter)
1346 return {f.LocalPath(): f.OldContents() for f in files}
1347
1348
1349class GitChange(Change):
1350 _AFFECTED_FILES = GitAffectedFile
1351 scm = 'git'
1352
1353 def AllFiles(self, root=None):
1354 """List all files under source control in the repo."""
1355 root = root or self.RepositoryRoot()
1356 return subprocess.check_output(
1357 ['git', '-c', 'core.quotePath=false', 'ls-files', '--', '.'],
1358 cwd=root).decode('utf-8', 'ignore').splitlines()
1359
1360
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001361def ListRelevantPresubmitFiles(files, root):
1362 """Finds all presubmit files that apply to a given set of source files.
1363
1364 If inherit-review-settings-ok is present right under root, looks for
1365 PRESUBMIT.py in directories enclosing root.
1366
1367 Args:
1368 files: An iterable container containing file paths.
1369 root: Path where to stop searching.
1370
1371 Return:
1372 List of absolute paths of the existing PRESUBMIT.py scripts.
1373 """
1374 files = [normpath(os.path.join(root, f)) for f in files]
1375
1376 # List all the individual directories containing files.
1377 directories = {os.path.dirname(f) for f in files}
1378
1379 # Ignore root if inherit-review-settings-ok is present.
1380 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1381 root = None
1382
1383 # Collect all unique directories that may contain PRESUBMIT.py.
1384 candidates = set()
1385 for directory in directories:
1386 while True:
1387 if directory in candidates:
1388 break
1389 candidates.add(directory)
1390 if directory == root:
1391 break
1392 parent_dir = os.path.dirname(directory)
1393 if parent_dir == directory:
1394 # We hit the system root directory.
1395 break
1396 directory = parent_dir
1397
1398 # Look for PRESUBMIT.py in all candidate directories.
1399 results = []
1400 for directory in sorted(list(candidates)):
1401 try:
1402 for f in os.listdir(directory):
1403 p = os.path.join(directory, f)
1404 if os.path.isfile(p) and re.match(
1405 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1406 results.append(p)
1407 except OSError:
1408 pass
1409
1410 logging.debug('Presubmit files: %s', ','.join(results))
1411 return results
1412
1413
rmistry@google.com5626a922015-02-26 14:03:30 +00001414class GetPostUploadExecuter(object):
Pavol Marko624e7ee2023-01-09 09:56:29 +00001415 def __init__(self, change, gerrit_obj, use_python3):
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001416 """
1417 Args:
Pavol Marko624e7ee2023-01-09 09:56:29 +00001418 change: The Change object.
1419 gerrit_obj: provides basic Gerrit codereview functionality.
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001420 use_python3: if true, will use python3 instead of python2 by default
1421 if USE_PYTHON3 is not specified.
1422 """
Pavol Marko624e7ee2023-01-09 09:56:29 +00001423 self.change = change
1424 self.gerrit = gerrit_obj
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001425 self.use_python3 = use_python3
1426
Pavol Marko624e7ee2023-01-09 09:56:29 +00001427 def ExecPresubmitScript(self, script_text, presubmit_path):
rmistry@google.com5626a922015-02-26 14:03:30 +00001428 """Executes PostUploadHook() from a single presubmit script.
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001429 Caller is responsible for validating whether the hook should be executed
1430 and should only call this function if it should be.
rmistry@google.com5626a922015-02-26 14:03:30 +00001431
1432 Args:
1433 script_text: The text of the presubmit script.
1434 presubmit_path: Project script to run.
rmistry@google.com5626a922015-02-26 14:03:30 +00001435
1436 Return:
1437 A list of results objects.
1438 """
Pavol Marko624e7ee2023-01-09 09:56:29 +00001439 # Change to the presubmit file's directory to support local imports.
1440 presubmit_dir = os.path.dirname(presubmit_path)
1441 main_path = os.getcwd()
1442 try:
1443 os.chdir(presubmit_dir)
1444 return self._execute_with_local_working_directory(script_text,
1445 presubmit_dir,
1446 presubmit_path)
1447 finally:
1448 # Return the process to the original working directory.
1449 os.chdir(main_path)
1450
1451 def _execute_with_local_working_directory(self, script_text, presubmit_dir,
1452 presubmit_path):
rmistry@google.com5626a922015-02-26 14:03:30 +00001453 context = {}
1454 try:
Pavol Marko624e7ee2023-01-09 09:56:29 +00001455 exec(compile(script_text, presubmit_path, 'exec', dont_inherit=True),
Raul Tambre09e64b42019-05-14 01:57:22 +00001456 context)
Raul Tambre7c938462019-05-24 16:35:35 +00001457 except Exception as e:
rmistry@google.com5626a922015-02-26 14:03:30 +00001458 raise PresubmitFailure('"%s" had an exception.\n%s'
1459 % (presubmit_path, e))
1460
1461 function_name = 'PostUploadHook'
1462 if function_name not in context:
1463 return {}
1464 post_upload_hook = context[function_name]
1465 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1466 raise PresubmitFailure(
1467 'Expected function "PostUploadHook" to take three arguments.')
Pavol Marko624e7ee2023-01-09 09:56:29 +00001468 return post_upload_hook(self.gerrit, self.change, OutputApi(False))
rmistry@google.com5626a922015-02-26 14:03:30 +00001469
1470
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001471def _MergeMasters(masters1, masters2):
1472 """Merges two master maps. Merges also the tests of each builder."""
1473 result = {}
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001474 for (master, builders) in itertools.chain(masters1.items(),
1475 masters2.items()):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001476 new_builders = result.setdefault(master, {})
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001477 for (builder, tests) in builders.items():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001478 new_builders.setdefault(builder, set([])).update(tests)
1479 return result
1480
1481
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001482def DoPostUploadExecuter(change, gerrit_obj, verbose, use_python3=False):
rmistry@google.com5626a922015-02-26 14:03:30 +00001483 """Execute the post upload hook.
1484
1485 Args:
1486 change: The Change object.
Edward Lemur016a0872020-02-04 22:13:28 +00001487 gerrit_obj: The GerritAccessor object.
rmistry@google.com5626a922015-02-26 14:03:30 +00001488 verbose: Prints debug info.
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001489 use_python3: if true, default to using Python3 for presubmit checks
1490 rather than Python2.
rmistry@google.com5626a922015-02-26 14:03:30 +00001491 """
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001492 python_version = 'Python %s' % sys.version_info.major
1493 sys.stdout.write('Running %s post upload checks ...\n' % python_version)
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001494 presubmit_files = ListRelevantPresubmitFiles(
1495 change.LocalPaths(), change.RepositoryRoot())
rmistry@google.com5626a922015-02-26 14:03:30 +00001496 if not presubmit_files and verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001497 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
rmistry@google.com5626a922015-02-26 14:03:30 +00001498 results = []
Pavol Marko624e7ee2023-01-09 09:56:29 +00001499 executer = GetPostUploadExecuter(change, gerrit_obj, use_python3)
rmistry@google.com5626a922015-02-26 14:03:30 +00001500 # The root presubmit file should be executed after the ones in subdirectories.
1501 # i.e. the specific post upload hooks should run before the general ones.
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001502 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
rmistry@google.com5626a922015-02-26 14:03:30 +00001503 presubmit_files.reverse()
1504
1505 for filename in presubmit_files:
1506 filename = os.path.abspath(filename)
rmistry@google.com5626a922015-02-26 14:03:30 +00001507 # Accept CRLF presubmit script.
Bruce Dawson6e70e862022-07-01 19:37:01 +00001508 presubmit_script = gclient_utils.FileRead(filename).replace('\r\n', '\n')
Josip Sokcevic48d8e902023-03-09 02:38:26 +00001509 if _ShouldRunPresubmit(presubmit_script, use_python3):
1510 if sys.version_info[0] == 2:
1511 sys.stdout.write(
1512 'Running %s under Python 2. Add USE_PYTHON3 = True to prevent '
1513 'this.\n' % filename)
1514 elif verbose:
1515 sys.stdout.write('Running %s\n' % filename)
1516 results.extend(executer.ExecPresubmitScript(presubmit_script, filename))
rmistry@google.com5626a922015-02-26 14:03:30 +00001517
Edward Lemur6eb1d322020-02-27 22:20:15 +00001518 if not results:
1519 return 0
1520
1521 sys.stdout.write('\n')
1522 sys.stdout.write('** Post Upload Hook Messages **\n')
1523
1524 exit_code = 0
1525 for result in results:
1526 if result.fatal:
1527 exit_code = 1
1528 result.handle()
1529 sys.stdout.write('\n')
1530
1531 return exit_code
rmistry@google.com5626a922015-02-26 14:03:30 +00001532
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001533class PresubmitExecuter(object):
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001534 def __init__(self, change, committing, verbose, gerrit_obj, dry_run=None,
Bruce Dawson09c0c072022-05-26 20:28:58 +00001535 thread_pool=None, parallel=False, use_python3=False,
1536 no_diffs=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001537 """
1538 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001539 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001540 committing: True if 'git cl land' is running, False if 'git cl upload' is.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001541 gerrit_obj: provides basic Gerrit codereview functionality.
1542 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001543 parallel: if true, all tests reported via input_api.RunTests for all
1544 PRESUBMIT files will be run in parallel.
Dirk Pranke6f0df682021-06-25 00:42:33 +00001545 use_python3: if true, will use python3 instead of python2 by default
1546 if USE_PYTHON3 is not specified.
Bruce Dawson09c0c072022-05-26 20:28:58 +00001547 no_diffs: if true, implies that --files or --all was specified so some
1548 checks can be skipped, and some errors will be messages.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001549 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001550 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001551 self.committing = committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001552 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001553 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001554 self.dry_run = dry_run
Daniel Cheng7227d212017-11-17 08:12:37 -08001555 self.more_cc = []
Edward Lesmes8e282792018-04-03 18:50:29 -04001556 self.thread_pool = thread_pool
1557 self.parallel = parallel
Dirk Pranke6f0df682021-06-25 00:42:33 +00001558 self.use_python3 = use_python3
Bruce Dawson09c0c072022-05-26 20:28:58 +00001559 self.no_diffs = no_diffs
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001560
1561 def ExecPresubmitScript(self, script_text, presubmit_path):
1562 """Executes a single presubmit script.
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001563 Caller is responsible for validating whether the hook should be executed
1564 and should only call this function if it should be.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001565
1566 Args:
1567 script_text: The text of the presubmit script.
1568 presubmit_path: The path to the presubmit file (this will be reported via
1569 input_api.PresubmitLocalPath()).
1570
1571 Return:
1572 A list of result objects, empty if no problems.
1573 """
chase@chromium.org8e416c82009-10-06 04:30:44 +00001574 # Change to the presubmit file's directory to support local imports.
Saagar Sanghavi98b332f2020-07-31 17:19:15 +00001575 presubmit_dir = os.path.dirname(presubmit_path)
Pavol Marko624e7ee2023-01-09 09:56:29 +00001576 main_path = os.getcwd()
1577 try:
1578 os.chdir(presubmit_dir)
1579 return self._execute_with_local_working_directory(script_text,
1580 presubmit_dir,
1581 presubmit_path)
1582 finally:
1583 # Return the process to the original working directory.
1584 os.chdir(main_path)
chase@chromium.org8e416c82009-10-06 04:30:44 +00001585
Pavol Marko624e7ee2023-01-09 09:56:29 +00001586 def _execute_with_local_working_directory(self, script_text, presubmit_dir,
1587 presubmit_path):
chase@chromium.org8e416c82009-10-06 04:30:44 +00001588 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001589 input_api = InputApi(self.change, presubmit_path, self.committing,
Aaron Gable668c1d82018-04-03 10:19:16 -07001590 self.verbose, gerrit_obj=self.gerrit,
Edward Lesmes8e282792018-04-03 18:50:29 -04001591 dry_run=self.dry_run, thread_pool=self.thread_pool,
Bruce Dawson09c0c072022-05-26 20:28:58 +00001592 parallel=self.parallel, no_diffs=self.no_diffs)
Daniel Cheng7227d212017-11-17 08:12:37 -08001593 output_api = OutputApi(self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001594 context = {}
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001595
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001596 try:
Bruce Dawson0ba2fd42022-07-21 13:47:21 +00001597 exec(compile(script_text, presubmit_path, 'exec', dont_inherit=True),
Raul Tambre09e64b42019-05-14 01:57:22 +00001598 context)
Raul Tambre7c938462019-05-24 16:35:35 +00001599 except Exception as e:
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001600 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001601
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001602 context['__args'] = (input_api, output_api)
Ben Pastene8351dc12020-08-06 05:01:35 +00001603
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001604 # Get path of presubmit directory relative to repository root.
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001605 # Always use forward slashes, so that path is same in *nix and Windows
1606 root = input_api.change.RepositoryRoot()
1607 rel_path = os.path.relpath(presubmit_dir, root)
1608 rel_path = rel_path.replace(os.path.sep, '/')
Ben Pastene8351dc12020-08-06 05:01:35 +00001609
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001610 # Get the URL of git remote origin and use it to identify host and project
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001611 host = project = ''
1612 if self.gerrit:
1613 host = self.gerrit.host or ''
1614 project = self.gerrit.project or ''
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001615
1616 # Prefix for test names
1617 prefix = 'presubmit:%s/%s:%s/' % (host, project, rel_path)
1618
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001619 # Perform all the desired presubmit checks.
1620 results = []
Ben Pastene8351dc12020-08-06 05:01:35 +00001621
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001622 try:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001623 version = [
1624 int(x) for x in context.get('PRESUBMIT_VERSION', '0.0.0').split('.')
1625 ]
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001626
Scott Leecc2fe9b2020-11-19 19:38:06 +00001627 with rdb_wrapper.client(prefix) as sink:
1628 if version >= [2, 0, 0]:
Andrew Grievebdfb8f22022-02-02 21:56:47 +00001629 # Copy the keys to prevent "dictionary changed size during iteration"
1630 # exception if checks add globals to context. E.g. sometimes the
1631 # Python runtime will add __warningregistry__.
1632 for function_name in list(context.keys()):
Scott Leecc2fe9b2020-11-19 19:38:06 +00001633 if not function_name.startswith('Check'):
1634 continue
1635 if function_name.endswith('Commit') and not self.committing:
1636 continue
1637 if function_name.endswith('Upload') and self.committing:
1638 continue
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001639 logging.debug('Running %s in %s', function_name, presubmit_path)
1640 results.extend(
Bruce Dawson8fa42e22022-03-29 17:05:38 +00001641 self._run_check_function(function_name, context, sink,
1642 presubmit_path))
Scott Leecc2fe9b2020-11-19 19:38:06 +00001643 logging.debug('Running %s done.', function_name)
1644 self.more_cc.extend(output_api.more_cc)
1645
1646 else: # Old format
1647 if self.committing:
1648 function_name = 'CheckChangeOnCommit'
1649 else:
1650 function_name = 'CheckChangeOnUpload'
Andrew Grievebdfb8f22022-02-02 21:56:47 +00001651 if function_name in list(context.keys()):
Scott Leecc2fe9b2020-11-19 19:38:06 +00001652 logging.debug('Running %s in %s', function_name, presubmit_path)
1653 results.extend(
Bruce Dawson8fa42e22022-03-29 17:05:38 +00001654 self._run_check_function(function_name, context, sink,
1655 presubmit_path))
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001656 logging.debug('Running %s done.', function_name)
1657 self.more_cc.extend(output_api.more_cc)
1658
1659 finally:
1660 for f in input_api._named_temporary_files:
1661 os.remove(f)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001662
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001663 return results
1664
Bruce Dawson8fa42e22022-03-29 17:05:38 +00001665 def _run_check_function(self, function_name, context, sink, presubmit_path):
Scott Leecc2fe9b2020-11-19 19:38:06 +00001666 """Evaluates and returns the result of a given presubmit function.
1667
1668 If sink is given, the result of the presubmit function will be reported
1669 to the ResultSink.
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001670
1671 Args:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001672 function_name: the name of the presubmit function to evaluate
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001673 context: a context dictionary in which the function will be evaluated
Scott Leecc2fe9b2020-11-19 19:38:06 +00001674 sink: an instance of ResultSink. None, by default.
1675 Returns:
1676 the result of the presubmit function call.
1677 """
1678 start_time = time_time()
1679 try:
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001680 result = eval(function_name + '(*__args)', context)
1681 self._check_result_type(result)
Allen Webbfe7d7092021-05-18 02:05:49 +00001682 except Exception:
Bruce Dawson10a82862022-05-27 19:25:56 +00001683 _, e_value, _ = sys.exc_info()
1684 result = [
1685 OutputApi.PresubmitError(
1686 'Evaluation of %s failed: %s, %s' %
1687 (function_name, e_value, traceback.format_exc()))
1688 ]
Scott Leecc2fe9b2020-11-19 19:38:06 +00001689
Bruce Dawson2bdc49f2021-05-21 19:13:03 +00001690 elapsed_time = time_time() - start_time
1691 if elapsed_time > 10.0:
Bruce Dawson6757d462022-07-13 04:04:40 +00001692 sys.stdout.write('%6.1fs to run %s from %s.\n' %
1693 (elapsed_time, function_name, presubmit_path))
Scott Leecc2fe9b2020-11-19 19:38:06 +00001694 if sink:
Erik Staab9f38b632022-10-31 14:05:24 +00001695 failure_reason = None
Scott Leecc2fe9b2020-11-19 19:38:06 +00001696 status = rdb_wrapper.STATUS_PASS
1697 if any(r.fatal for r in result):
1698 status = rdb_wrapper.STATUS_FAIL
Erik Staab9f38b632022-10-31 14:05:24 +00001699 failure_reasons = []
1700 for r in result:
1701 fields = r.json_format()
1702 message = fields['message']
1703 items = '\n'.join(' %s' % item for item in fields['items'])
1704 failure_reasons.append('\n'.join([message, items]))
1705 if failure_reasons:
1706 failure_reason = '\n'.join(failure_reasons)
1707 sink.report(function_name, status, elapsed_time, failure_reason)
Scott Leecc2fe9b2020-11-19 19:38:06 +00001708
1709 return result
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001710
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001711 def _check_result_type(self, result):
1712 """Helper function which ensures result is a list, and all elements are
1713 instances of OutputApi.PresubmitResult"""
1714 if not isinstance(result, (tuple, list)):
1715 raise PresubmitFailure('Presubmit functions must return a tuple or list')
1716 if not all(isinstance(res, OutputApi.PresubmitResult) for res in result):
1717 raise PresubmitFailure(
1718 'All presubmit results must be of types derived from '
1719 'output_api.PresubmitResult')
1720
1721
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001722def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001723 committing,
1724 verbose,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001725 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001726 may_prompt,
Aaron Gable668c1d82018-04-03 10:19:16 -07001727 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04001728 dry_run=None,
Debrian Figueroadd2737e2019-06-21 23:50:13 +00001729 parallel=False,
Dirk Pranke6f0df682021-06-25 00:42:33 +00001730 json_output=None,
Bruce Dawson09c0c072022-05-26 20:28:58 +00001731 use_python3=False,
1732 no_diffs=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001733 """Runs all presubmit checks that apply to the files in the change.
1734
1735 This finds all PRESUBMIT.py files in directories enclosing the files in the
1736 change (up to the repository root) and calls the relevant entrypoint function
1737 depending on whether the change is being committed or uploaded.
1738
1739 Prints errors, warnings and notifications. Prompts the user for warnings
1740 when needed.
1741
1742 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001743 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001744 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001745 verbose: Prints debug info.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001746 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001747 may_prompt: Enable (y/n) questions on warning or error. If False,
1748 any questions are answered with yes by default.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001749 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001750 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001751 parallel: if true, all tests specified by input_api.RunTests in all
1752 PRESUBMIT files will be run in parallel.
Dirk Pranke6f0df682021-06-25 00:42:33 +00001753 use_python3: if true, default to using Python3 for presubmit checks
1754 rather than Python2.
Bruce Dawson09c0c072022-05-26 20:28:58 +00001755 no_diffs: if true, implies that --files or --all was specified so some
1756 checks can be skipped, and some errors will be messages.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001757 Return:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001758 1 if presubmit checks failed or 0 otherwise.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001759 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001760 old_environ = os.environ
1761 try:
1762 # Make sure python subprocesses won't generate .pyc files.
1763 os.environ = os.environ.copy()
Edward Lemurb9830242019-10-30 22:19:20 +00001764 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001765
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001766 python_version = 'Python %s' % sys.version_info.major
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001767 if committing:
Dirk Pranke6fc394f2021-05-26 23:25:14 +00001768 sys.stdout.write('Running %s presubmit commit checks ...\n' %
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001769 python_version)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001770 else:
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001771 sys.stdout.write('Running %s presubmit upload checks ...\n' %
1772 python_version)
Edward Lemurecc27072020-01-06 16:42:34 +00001773 start_time = time_time()
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001774 presubmit_files = ListRelevantPresubmitFiles(
1775 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001776 if not presubmit_files and verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001777 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001778 results = []
Bruce Dawsonc9f904f2022-10-14 20:59:49 +00001779 if sys.platform == 'win32':
1780 temp = os.environ['TEMP']
1781 else:
1782 temp = '/tmp'
1783 python2_usage_log_file = os.path.join(temp, 'python2_usage.txt')
Bruce Dawsonf2b04212022-06-10 16:42:54 +00001784 if os.path.exists(python2_usage_log_file):
1785 os.remove(python2_usage_log_file)
Edward Lesmes8e282792018-04-03 18:50:29 -04001786 thread_pool = ThreadPool()
Edward Lemur7e3c67f2018-07-20 20:52:49 +00001787 executer = PresubmitExecuter(change, committing, verbose, gerrit_obj,
Bruce Dawson09c0c072022-05-26 20:28:58 +00001788 dry_run, thread_pool, parallel, use_python3,
1789 no_diffs)
Josip Sokcevic48d8e902023-03-09 02:38:26 +00001790 skipped_count = 0;
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001791 if default_presubmit:
1792 if verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001793 sys.stdout.write('Running default presubmit script.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001794 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
Josip Sokcevic48d8e902023-03-09 02:38:26 +00001795 if _ShouldRunPresubmit(default_presubmit, use_python3):
1796 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1797 else:
1798 skipped_count += 1
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001799 for filename in presubmit_files:
1800 filename = os.path.abspath(filename)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001801 # Accept CRLF presubmit script.
Bruce Dawson6e70e862022-07-01 19:37:01 +00001802 presubmit_script = gclient_utils.FileRead(filename).replace('\r\n', '\n')
Josip Sokcevic48d8e902023-03-09 02:38:26 +00001803 if _ShouldRunPresubmit(presubmit_script, use_python3):
1804 if sys.version_info[0] == 2:
1805 sys.stdout.write(
1806 'Running %s under Python 2. Add USE_PYTHON3 = True to prevent '
1807 'this.\n' % filename)
1808 elif verbose:
1809 sys.stdout.write('Running %s\n' % filename)
1810 results += executer.ExecPresubmitScript(presubmit_script, filename)
1811 else:
1812 skipped_count += 1
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001813
Edward Lesmes8e282792018-04-03 18:50:29 -04001814 results += thread_pool.RunAsync()
1815
Bruce Dawsonf2b04212022-06-10 16:42:54 +00001816 if os.path.exists(python2_usage_log_file):
1817 with open(python2_usage_log_file) as f:
1818 python2_usage = [x.strip() for x in f.readlines()]
1819 results.append(
1820 OutputApi(committing).PresubmitPromptWarning(
1821 'Python 2 scripts were run during %s presubmits. Please see '
1822 'https://bugs.chromium.org/p/chromium/issues/detail?id=1313804'
1823 '#c61 for tips on resolving this.'
1824 % python_version,
1825 items=python2_usage))
1826
Edward Lemur6eb1d322020-02-27 22:20:15 +00001827 messages = {}
1828 should_prompt = False
1829 presubmits_failed = False
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001830 for result in results:
1831 if result.fatal:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001832 presubmits_failed = True
1833 messages.setdefault('ERRORS', []).append(result)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001834 elif result.should_prompt:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001835 should_prompt = True
1836 messages.setdefault('Warnings', []).append(result)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001837 else:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001838 messages.setdefault('Messages', []).append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001839
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001840 # Print the different message types in a consistent order. ERRORS go last
1841 # so that they will be most visible in the local-presubmit output.
1842 for name in ['Messages', 'Warnings', 'ERRORS']:
1843 if name in messages:
1844 items = messages[name]
Gavin Makd22bf602022-07-11 21:10:41 +00001845 sys.stdout.write('** Presubmit %s: %d **\n' % (name, len(items)))
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001846 for item in items:
1847 item.handle()
1848 sys.stdout.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001849
Edward Lemurecc27072020-01-06 16:42:34 +00001850 total_time = time_time() - start_time
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001851 if total_time > 1.0:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001852 sys.stdout.write(
Louis Romero4ca9e1c2022-03-10 10:29:11 +00001853 'Presubmit checks took %.1fs to calculate.\n' % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001854
Edward Lemur6eb1d322020-02-27 22:20:15 +00001855 if not should_prompt and not presubmits_failed:
Louis Romero4ca9e1c2022-03-10 10:29:11 +00001856 sys.stdout.write('%s presubmit checks passed.\n\n' % python_version)
Josip Sokcevic7592e0a2022-01-12 00:57:54 +00001857 elif should_prompt and not presubmits_failed:
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001858 sys.stdout.write('There were %s presubmit warnings. ' % python_version)
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001859 if may_prompt:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001860 presubmits_failed = not prompt_should_continue(
1861 'Are you sure you wish to continue? (y/N): ')
Garrett Beatyacb807e2020-08-28 18:17:24 +00001862 else:
1863 sys.stdout.write('\n')
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001864 else:
1865 sys.stdout.write('There were %s presubmit errors.\n' % python_version)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001866
Edward Lemur1dc66e12020-02-21 21:36:34 +00001867 if json_output:
1868 # Write the presubmit results to json output
1869 presubmit_results = {
1870 'errors': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001871 error.json_format()
1872 for error in messages.get('ERRORS', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001873 ],
1874 'notifications': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001875 notification.json_format()
1876 for notification in messages.get('Messages', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001877 ],
1878 'warnings': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001879 warning.json_format()
1880 for warning in messages.get('Warnings', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001881 ],
Edward Lemur6eb1d322020-02-27 22:20:15 +00001882 'more_cc': executer.more_cc,
Josip Sokcevic48d8e902023-03-09 02:38:26 +00001883 'skipped_presubmits': skipped_count,
Edward Lemur1dc66e12020-02-21 21:36:34 +00001884 }
1885
1886 gclient_utils.FileWrite(
1887 json_output, json.dumps(presubmit_results, sort_keys=True))
1888
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001889 global _ASKED_FOR_FEEDBACK
1890 # Ask for feedback one time out of 5.
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001891 if (results and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
Edward Lemur6eb1d322020-02-27 22:20:15 +00001892 sys.stdout.write(
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001893 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1894 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1895 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001896 _ASKED_FOR_FEEDBACK = True
Edward Lemur6eb1d322020-02-27 22:20:15 +00001897
1898 return 1 if presubmits_failed else 0
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001899 finally:
1900 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001901
1902
Edward Lemur50984a62020-02-06 18:10:18 +00001903def _scan_sub_dirs(mask, recursive):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001904 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001905 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001906
1907 results = []
1908 for root, dirs, files in os.walk('.'):
1909 if '.svn' in dirs:
1910 dirs.remove('.svn')
1911 if '.git' in dirs:
1912 dirs.remove('.git')
1913 for name in files:
1914 if fnmatch.fnmatch(name, mask):
1915 results.append(os.path.join(root, name))
1916 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001917
1918
Edward Lemur50984a62020-02-06 18:10:18 +00001919def _parse_files(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001920 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001921 files = []
1922 for arg in args:
Edward Lemur50984a62020-02-06 18:10:18 +00001923 files.extend([('M', f) for f in _scan_sub_dirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001924 return files
1925
1926
Edward Lemur50984a62020-02-06 18:10:18 +00001927def _parse_change(parser, options):
1928 """Process change options.
1929
1930 Args:
1931 parser: The parser used to parse the arguments from command line.
1932 options: The arguments parsed from command line.
1933 Returns:
Josip Sokcevic7958e302023-03-01 23:02:21 +00001934 A GitChange if the change root is a git repository, or a Change otherwise.
Edward Lemur50984a62020-02-06 18:10:18 +00001935 """
1936 if options.files and options.all_files:
1937 parser.error('<files> cannot be specified when --all-files is set.')
1938
Edward Lesmes44ea3ff2020-02-05 00:14:30 +00001939 change_scm = scm.determine_scm(options.root)
Edward Lemur50984a62020-02-06 18:10:18 +00001940 if change_scm != 'git' and not options.files:
1941 parser.error('<files> is not optional for unversioned directories.')
1942
1943 if options.files:
Josip Sokcevic017544d2022-03-31 23:47:53 +00001944 if options.source_controlled_only:
1945 # Get the filtered set of files from SCM.
1946 change_files = []
1947 for name in scm.GIT.GetAllFiles(options.root):
1948 for mask in options.files:
1949 if fnmatch.fnmatch(name, mask):
1950 change_files.append(('M', name))
1951 break
1952 else:
1953 # Get the filtered set of files from a directory scan.
1954 change_files = _parse_files(options.files, options.recursive)
Edward Lemur50984a62020-02-06 18:10:18 +00001955 elif options.all_files:
1956 change_files = [('M', f) for f in scm.GIT.GetAllFiles(options.root)]
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001957 else:
Edward Lemur50984a62020-02-06 18:10:18 +00001958 change_files = scm.GIT.CaptureStatus(
Edward Lemur7f6dec02020-02-06 20:23:58 +00001959 options.root, options.upstream or None)
Edward Lemur50984a62020-02-06 18:10:18 +00001960
1961 logging.info('Found %d file(s).', len(change_files))
1962
Josip Sokcevic7958e302023-03-01 23:02:21 +00001963 change_class = GitChange if change_scm == 'git' else Change
Edward Lemur50984a62020-02-06 18:10:18 +00001964 return change_class(
1965 options.name,
1966 options.description,
1967 options.root,
1968 change_files,
1969 options.issue,
1970 options.patchset,
1971 options.author,
1972 upstream=options.upstream)
1973
1974
1975def _parse_gerrit_options(parser, options):
1976 """Process gerrit options.
1977
1978 SIDE EFFECTS: Modifies options.author and options.description from Gerrit if
1979 options.gerrit_fetch is set.
1980
1981 Args:
1982 parser: The parser used to parse the arguments from command line.
1983 options: The arguments parsed from command line.
1984 Returns:
Stephen Martinisfb09de22021-02-25 03:41:13 +00001985 A GerritAccessor object if options.gerrit_url is set, or None otherwise.
Edward Lemur50984a62020-02-06 18:10:18 +00001986 """
1987 gerrit_obj = None
Stephen Martinisfb09de22021-02-25 03:41:13 +00001988 if options.gerrit_url:
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001989 gerrit_obj = GerritAccessor(
1990 url=options.gerrit_url,
1991 project=options.gerrit_project,
1992 branch=options.gerrit_branch)
Edward Lemur50984a62020-02-06 18:10:18 +00001993
1994 if not options.gerrit_fetch:
1995 return gerrit_obj
1996
1997 if not options.gerrit_url or not options.issue or not options.patchset:
1998 parser.error(
1999 '--gerrit_fetch requires --gerrit_url, --issue and --patchset.')
2000
2001 options.author = gerrit_obj.GetChangeOwner(options.issue)
2002 options.description = gerrit_obj.GetChangeDescription(
2003 options.issue, options.patchset)
2004
2005 logging.info('Got author: "%s"', options.author)
2006 logging.info('Got description: """\n%s\n"""', options.description)
2007
2008 return gerrit_obj
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00002009
2010
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002011@contextlib.contextmanager
2012def canned_check_filter(method_names):
2013 filtered = {}
2014 try:
2015 for method_name in method_names:
2016 if not hasattr(presubmit_canned_checks, method_name):
Gavin Make6a62332020-12-04 21:57:10 +00002017 logging.warning('Skipping unknown "canned" check %s' % method_name)
Aaron Gableecee74c2018-04-02 15:13:08 -07002018 continue
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002019 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
2020 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
2021 yield
2022 finally:
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002023 for name, method in filtered.items():
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002024 setattr(presubmit_canned_checks, name, method)
2025
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00002026
sbc@chromium.org013731e2015-02-26 18:28:43 +00002027def main(argv=None):
Edward Lemura5799e32020-01-17 19:26:51 +00002028 parser = argparse.ArgumentParser(usage='%(prog)s [options] <files...>')
2029 hooks = parser.add_mutually_exclusive_group()
2030 hooks.add_argument('-c', '--commit', action='store_true',
2031 help='Use commit instead of upload checks.')
2032 hooks.add_argument('-u', '--upload', action='store_false', dest='commit',
2033 help='Use upload instead of commit checks.')
Edward Lemur75526302020-02-27 22:31:05 +00002034 hooks.add_argument('--post_upload', action='store_true',
2035 help='Run post-upload commit hooks.')
Edward Lemura5799e32020-01-17 19:26:51 +00002036 parser.add_argument('-r', '--recursive', action='store_true',
2037 help='Act recursively.')
2038 parser.add_argument('-v', '--verbose', action='count', default=0,
2039 help='Use 2 times for more debug info.')
2040 parser.add_argument('--name', default='no name')
2041 parser.add_argument('--author')
Edward Lemur227d5102020-02-25 23:45:35 +00002042 desc = parser.add_mutually_exclusive_group()
2043 desc.add_argument('--description', default='', help='The change description.')
2044 desc.add_argument('--description_file',
2045 help='File to read change description from.')
Edward Lemura5799e32020-01-17 19:26:51 +00002046 parser.add_argument('--issue', type=int, default=0)
2047 parser.add_argument('--patchset', type=int, default=0)
2048 parser.add_argument('--root', default=os.getcwd(),
2049 help='Search for PRESUBMIT.py up to this directory. '
2050 'If inherit-review-settings-ok is present in this '
2051 'directory, parent directories up to the root file '
2052 'system directories will also be searched.')
2053 parser.add_argument('--upstream',
2054 help='Git only: the base ref or upstream branch against '
2055 'which the diff should be computed.')
2056 parser.add_argument('--default_presubmit')
2057 parser.add_argument('--may_prompt', action='store_true', default=False)
2058 parser.add_argument('--skip_canned', action='append', default=[],
2059 help='A list of checks to skip which appear in '
2060 'presubmit_canned_checks. Can be provided multiple times '
2061 'to skip multiple canned checks.')
2062 parser.add_argument('--dry_run', action='store_true', help=argparse.SUPPRESS)
2063 parser.add_argument('--gerrit_url', help=argparse.SUPPRESS)
Edward Lesmeseb1bd622021-03-01 19:54:07 +00002064 parser.add_argument('--gerrit_project', help=argparse.SUPPRESS)
2065 parser.add_argument('--gerrit_branch', help=argparse.SUPPRESS)
Edward Lemura5799e32020-01-17 19:26:51 +00002066 parser.add_argument('--gerrit_fetch', action='store_true',
2067 help=argparse.SUPPRESS)
2068 parser.add_argument('--parallel', action='store_true',
2069 help='Run all tests specified by input_api.RunTests in '
2070 'all PRESUBMIT files in parallel.')
2071 parser.add_argument('--json_output',
2072 help='Write presubmit errors to json output.')
Edward Lemur1dc66e12020-02-21 21:36:34 +00002073 parser.add_argument('--all_files', action='store_true',
Edward Lemur50984a62020-02-06 18:10:18 +00002074 help='Mark all files under source control as modified.')
Bruce Dawson09c0c072022-05-26 20:28:58 +00002075
Edward Lemura5799e32020-01-17 19:26:51 +00002076 parser.add_argument('files', nargs='*',
2077 help='List of files to be marked as modified when '
2078 'executing presubmit or post-upload hooks. fnmatch '
2079 'wildcards can also be used.')
Josip Sokcevic017544d2022-03-31 23:47:53 +00002080 parser.add_argument('--source_controlled_only', action='store_true',
2081 help='Constrain \'files\' to those in source control.')
Dirk Pranke6f0df682021-06-25 00:42:33 +00002082 parser.add_argument('--use-python3', action='store_true',
2083 help='Use python3 for presubmit checks by default')
Bruce Dawson09c0c072022-05-26 20:28:58 +00002084 parser.add_argument('--no_diffs', action='store_true',
2085 help='Assume that all "modified" files have no diffs.')
Edward Lemura5799e32020-01-17 19:26:51 +00002086 options = parser.parse_args(argv)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00002087
Erik Staabcca5c492020-04-16 17:40:07 +00002088 log_level = logging.ERROR
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002089 if options.verbose >= 2:
Erik Staabcca5c492020-04-16 17:40:07 +00002090 log_level = logging.DEBUG
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002091 elif options.verbose:
Erik Staabcca5c492020-04-16 17:40:07 +00002092 log_level = logging.INFO
2093 log_format = ('[%(levelname).1s%(asctime)s %(process)d %(thread)d '
2094 '%(filename)s] %(message)s')
2095 logging.basicConfig(format=log_format, level=log_level)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00002096
Bruce Dawsondca14bc2022-09-15 20:59:38 +00002097 # Print call stacks when _PresubmitResult objects are created with -v -v is
2098 # specified. This helps track down where presubmit messages are coming from.
2099 if options.verbose >= 2:
2100 global _SHOW_CALLSTACKS
2101 _SHOW_CALLSTACKS = True
2102
Edward Lemur227d5102020-02-25 23:45:35 +00002103 if options.description_file:
2104 options.description = gclient_utils.FileRead(options.description_file)
Edward Lemur50984a62020-02-06 18:10:18 +00002105 gerrit_obj = _parse_gerrit_options(parser, options)
2106 change = _parse_change(parser, options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00002107
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002108 try:
Edward Lemur75526302020-02-27 22:31:05 +00002109 if options.post_upload:
Josip Sokcevice293d3d2022-02-16 22:52:15 +00002110 return DoPostUploadExecuter(change, gerrit_obj, options.verbose,
2111 options.use_python3)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002112 with canned_check_filter(options.skip_canned):
Edward Lemur6eb1d322020-02-27 22:20:15 +00002113 return DoPresubmitChecks(
Edward Lemur50984a62020-02-06 18:10:18 +00002114 change,
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002115 options.commit,
2116 options.verbose,
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002117 options.default_presubmit,
2118 options.may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002119 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04002120 options.dry_run,
Debrian Figueroadd2737e2019-06-21 23:50:13 +00002121 options.parallel,
Dirk Pranke6f0df682021-06-25 00:42:33 +00002122 options.json_output,
Bruce Dawson09c0c072022-05-26 20:28:58 +00002123 options.use_python3,
2124 options.no_diffs)
Raul Tambre7c938462019-05-24 16:35:35 +00002125 except PresubmitFailure as e:
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00002126 import utils
Raul Tambre80ee78e2019-05-06 22:41:05 +00002127 print(e, file=sys.stderr)
2128 print('Maybe your depot_tools is out of date?', file=sys.stderr)
Josip Sokcevic0399e172022-03-21 23:11:51 +00002129 print('depot_tools version: %s' % utils.depot_tools_version(),
2130 file=sys.stderr)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002131 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002132
2133
2134if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00002135 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00002136 try:
2137 sys.exit(main())
2138 except KeyboardInterrupt:
2139 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07002140 sys.exit(2)