blob: d3a5b58f63075821df5911864ead915a87311eb6 [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
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000045import gerrit_util
Edward Lesmes9ce03f82021-01-12 20:13:31 +000046import owners_client
Jochen Eisinger76f5fc62017-04-07 16:27:46 +020047import owners_finder
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000048import presubmit_canned_checks
Saagar Sanghavi9949ab72020-07-20 20:56:40 +000049import rdb_wrapper
Josip Sokcevic688adfe2023-03-01 22:46:12 +000050from lib import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000051import subprocess2 as subprocess # Exposed through the API.
Josip Sokcevic688adfe2023-03-01 22:46:12 +000052
Josip Sokcevic512fd3b2023-03-01 21:06:41 +000053from lib import utils
Josip Sokcevic688adfe2023-03-01 22:46:12 +000054from lib import change as libchange
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000055
Edward Lemur16af3562019-10-17 22:11:33 +000056if sys.version_info.major == 2:
57 # TODO(1009814): Expose urllib2 only through urllib_request and urllib_error
58 import urllib2 # Exposed through the API.
59 import urlparse
60 import urllib2 as urllib_request
61 import urllib2 as urllib_error
62else:
63 import urllib.parse as urlparse
64 import urllib.request as urllib_request
65 import urllib.error as urllib_error
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000066
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000067# Ask for feedback only once in program lifetime.
68_ASKED_FOR_FEEDBACK = False
69
Bruce Dawsondca14bc2022-09-15 20:59:38 +000070# Set if super-verbose mode is requested, for tracking where presubmit messages
71# are coming from.
72_SHOW_CALLSTACKS = False
73
Josip Sokcevic512fd3b2023-03-01 21:06:41 +000074_PRESUBMIT_FILE_REGEX = r'PRESUBMIT.*\.py$'
75_PRESUBMIT_FILE_EXCLUDE = r'PRESUBMIT_test'
76
Bruce Dawsondca14bc2022-09-15 20:59:38 +000077
Edward Lemurecc27072020-01-06 16:42:34 +000078def time_time():
79 # Use this so that it can be mocked in tests without interfering with python
80 # system machinery.
81 return time.time()
82
83
maruel@chromium.org899e1c12011-04-07 17:03:18 +000084class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000085 pass
86
87
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000088class CommandData(object):
Edward Lemur940c2822019-08-23 00:34:25 +000089 def __init__(self, name, cmd, kwargs, message, python3=False):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000090 self.name = name
91 self.cmd = cmd
Edward Lesmes8e282792018-04-03 18:50:29 -040092 self.stdin = kwargs.get('stdin', None)
Edward Lemur2d6b67c2019-08-23 22:25:41 +000093 self.kwargs = kwargs.copy()
Edward Lesmes8e282792018-04-03 18:50:29 -040094 self.kwargs['stdout'] = subprocess.PIPE
95 self.kwargs['stderr'] = subprocess.STDOUT
96 self.kwargs['stdin'] = subprocess.PIPE
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000097 self.message = message
98 self.info = None
Edward Lemur940c2822019-08-23 00:34:25 +000099 self.python3 = python3
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000100
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000101
Edward Lesmes8e282792018-04-03 18:50:29 -0400102# Adapted from
103# https://github.com/google/gtest-parallel/blob/master/gtest_parallel.py#L37
104#
105# An object that catches SIGINT sent to the Python process and notices
106# if processes passed to wait() die by SIGINT (we need to look for
107# both of those cases, because pressing Ctrl+C can result in either
108# the main process or one of the subprocesses getting the signal).
109#
110# Before a SIGINT is seen, wait(p) will simply call p.wait() and
111# return the result. Once a SIGINT has been seen (in the main process
112# or a subprocess, including the one the current call is waiting for),
Edward Lemur9a5bb612019-09-26 02:01:52 +0000113# wait(p) will call p.terminate().
Edward Lesmes8e282792018-04-03 18:50:29 -0400114class SigintHandler(object):
Edward Lesmes8e282792018-04-03 18:50:29 -0400115 sigint_returncodes = {-signal.SIGINT, # Unix
116 -1073741510, # Windows
117 }
118 def __init__(self):
119 self.__lock = threading.Lock()
120 self.__processes = set()
121 self.__got_sigint = False
Edward Lemur9a5bb612019-09-26 02:01:52 +0000122 self.__previous_signal = signal.signal(signal.SIGINT, self.interrupt)
Edward Lesmes8e282792018-04-03 18:50:29 -0400123
124 def __on_sigint(self):
125 self.__got_sigint = True
126 while self.__processes:
127 try:
128 self.__processes.pop().terminate()
129 except OSError:
130 pass
131
Edward Lemur9a5bb612019-09-26 02:01:52 +0000132 def interrupt(self, signal_num, frame):
Edward Lesmes8e282792018-04-03 18:50:29 -0400133 with self.__lock:
134 self.__on_sigint()
Edward Lemur9a5bb612019-09-26 02:01:52 +0000135 self.__previous_signal(signal_num, frame)
Edward Lesmes8e282792018-04-03 18:50:29 -0400136
137 def got_sigint(self):
138 with self.__lock:
139 return self.__got_sigint
140
141 def wait(self, p, stdin):
142 with self.__lock:
143 if self.__got_sigint:
144 p.terminate()
145 self.__processes.add(p)
146 stdout, stderr = p.communicate(stdin)
147 code = p.returncode
148 with self.__lock:
149 self.__processes.discard(p)
150 if code in self.sigint_returncodes:
151 self.__on_sigint()
Edward Lesmes8e282792018-04-03 18:50:29 -0400152 return stdout, stderr
153
154sigint_handler = SigintHandler()
155
156
Edward Lemurecc27072020-01-06 16:42:34 +0000157class Timer(object):
158 def __init__(self, timeout, fn):
159 self.completed = False
160 self._fn = fn
161 self._timer = threading.Timer(timeout, self._onTimer) if timeout else None
162
163 def __enter__(self):
164 if self._timer:
165 self._timer.start()
166 return self
167
168 def __exit__(self, _type, _value, _traceback):
169 if self._timer:
170 self._timer.cancel()
171
172 def _onTimer(self):
173 self._fn()
174 self.completed = True
175
176
Edward Lesmes8e282792018-04-03 18:50:29 -0400177class ThreadPool(object):
Edward Lemurecc27072020-01-06 16:42:34 +0000178 def __init__(self, pool_size=None, timeout=None):
179 self.timeout = timeout
Edward Lesmes8e282792018-04-03 18:50:29 -0400180 self._pool_size = pool_size or multiprocessing.cpu_count()
Bruce Dawson8254f062022-06-17 17:33:08 +0000181 if sys.platform == 'win32':
182 # TODO(crbug.com/1190269) - we can't use more than 56 child processes on
183 # Windows or Python3 may hang.
184 self._pool_size = min(self._pool_size, 56)
Edward Lesmes8e282792018-04-03 18:50:29 -0400185 self._messages = []
186 self._messages_lock = threading.Lock()
187 self._tests = []
188 self._tests_lock = threading.Lock()
189 self._nonparallel_tests = []
190
Edward Lemurecc27072020-01-06 16:42:34 +0000191 def _GetCommand(self, test):
Edward Lemur940c2822019-08-23 00:34:25 +0000192 vpython = 'vpython'
193 if test.python3:
194 vpython += '3'
195 if sys.platform == 'win32':
196 vpython += '.bat'
Edward Lesmes8e282792018-04-03 18:50:29 -0400197
198 cmd = test.cmd
199 if cmd[0] == 'python':
200 cmd = list(cmd)
201 cmd[0] = vpython
202 elif cmd[0].endswith('.py'):
203 cmd = [vpython] + cmd
204
Edward Lemur336e51f2019-11-14 21:42:04 +0000205 # On Windows, scripts on the current directory take precedence over PATH, so
206 # that when testing depot_tools on Windows, calling `vpython.bat` will
207 # execute the copy of vpython of the depot_tools under test instead of the
208 # one in the bot.
209 # As a workaround, we run the tests from the parent directory instead.
210 if (cmd[0] == vpython and
211 'cwd' in test.kwargs and
212 os.path.basename(test.kwargs['cwd']) == 'depot_tools'):
213 test.kwargs['cwd'] = os.path.dirname(test.kwargs['cwd'])
214 cmd[1] = os.path.join('depot_tools', cmd[1])
215
Edward Lemurecc27072020-01-06 16:42:34 +0000216 return cmd
217
218 def _RunWithTimeout(self, cmd, stdin, kwargs):
219 p = subprocess.Popen(cmd, **kwargs)
220 with Timer(self.timeout, p.terminate) as timer:
221 stdout, _ = sigint_handler.wait(p, stdin)
Josip Sokcevic6635baf2021-11-19 02:04:39 +0000222 stdout = stdout.decode('utf-8', 'ignore')
Edward Lemurecc27072020-01-06 16:42:34 +0000223 if timer.completed:
224 stdout = 'Process timed out after %ss\n%s' % (self.timeout, stdout)
Josip Sokcevic6635baf2021-11-19 02:04:39 +0000225 return p.returncode, stdout
Edward Lemurecc27072020-01-06 16:42:34 +0000226
227 def CallCommand(self, test):
228 """Runs an external program.
229
Edward Lemura5799e32020-01-17 19:26:51 +0000230 This function converts invocation of .py files and invocations of 'python'
Edward Lemurecc27072020-01-06 16:42:34 +0000231 to vpython invocations.
232 """
233 cmd = self._GetCommand(test)
Edward Lesmes8e282792018-04-03 18:50:29 -0400234 try:
Edward Lemurecc27072020-01-06 16:42:34 +0000235 start = time_time()
236 returncode, stdout = self._RunWithTimeout(cmd, test.stdin, test.kwargs)
237 duration = time_time() - start
Edward Lemur7cf94382019-11-15 22:36:41 +0000238 except Exception:
Edward Lemurecc27072020-01-06 16:42:34 +0000239 duration = time_time() - start
Edward Lesmes8e282792018-04-03 18:50:29 -0400240 return test.message(
Edward Lemur7cf94382019-11-15 22:36:41 +0000241 '%s\n%s exec failure (%4.2fs)\n%s' % (
242 test.name, ' '.join(cmd), duration, traceback.format_exc()))
Edward Lemurde9e3ca2019-10-24 21:13:31 +0000243
Edward Lemurecc27072020-01-06 16:42:34 +0000244 if returncode != 0:
Edward Lesmes8e282792018-04-03 18:50:29 -0400245 return test.message(
Edward Lemur7cf94382019-11-15 22:36:41 +0000246 '%s\n%s (%4.2fs) failed\n%s' % (
247 test.name, ' '.join(cmd), duration, stdout))
Edward Lemurecc27072020-01-06 16:42:34 +0000248
Edward Lesmes8e282792018-04-03 18:50:29 -0400249 if test.info:
Edward Lemur7cf94382019-11-15 22:36:41 +0000250 return test.info('%s\n%s (%4.2fs)' % (test.name, ' '.join(cmd), duration))
Edward Lesmes8e282792018-04-03 18:50:29 -0400251
252 def AddTests(self, tests, parallel=True):
253 if parallel:
254 self._tests.extend(tests)
255 else:
256 self._nonparallel_tests.extend(tests)
257
258 def RunAsync(self):
259 self._messages = []
260
261 def _WorkerFn():
262 while True:
263 test = None
264 with self._tests_lock:
265 if not self._tests:
266 break
267 test = self._tests.pop()
268 result = self.CallCommand(test)
269 if result:
270 with self._messages_lock:
271 self._messages.append(result)
272
273 def _StartDaemon():
274 t = threading.Thread(target=_WorkerFn)
275 t.daemon = True
276 t.start()
277 return t
278
279 while self._nonparallel_tests:
280 test = self._nonparallel_tests.pop()
281 result = self.CallCommand(test)
282 if result:
283 self._messages.append(result)
284
285 if self._tests:
286 threads = [_StartDaemon() for _ in range(self._pool_size)]
287 for worker in threads:
288 worker.join()
289
290 return self._messages
291
292
Edward Lemur6eb1d322020-02-27 22:20:15 +0000293def prompt_should_continue(prompt_string):
294 sys.stdout.write(prompt_string)
Dirk Pranke7288f882021-06-03 18:01:30 +0000295 sys.stdout.flush()
Edward Lemur6eb1d322020-02-27 22:20:15 +0000296 response = sys.stdin.readline().strip().lower()
297 return response in ('y', 'yes')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000298
299
Josip Sokcevice293d3d2022-02-16 22:52:15 +0000300def _ShouldRunPresubmit(script_text, use_python3):
301 """Try to figure out whether these presubmit checks should be run under
302 python2 or python3. We need to do this without actually trying to
303 compile the text, since the text might compile in one but not the
304 other.
305
306 Args:
307 script_text: The text of the presubmit script.
308 use_python3: if true, will use python3 instead of python2 by default
309 if USE_PYTHON3 is not specified.
310
311 Return:
312 A boolean if presubmit should be executed
313 """
Josip Sokcevic884d7162023-01-19 21:17:56 +0000314 if os.getenv('LUCI_OMIT_PYTHON2') == 'true':
315 # If LUCI omits python2, run all presubmits with python3, regardless of
316 # USE_PYTHON3 variable.
317 return True
318
Josip Sokcevice293d3d2022-02-16 22:52:15 +0000319 m = re.search('^USE_PYTHON3 = (True|False)$', script_text, flags=re.MULTILINE)
320 if m:
321 use_python3 = m.group(1) == 'True'
322
323 return ((sys.version_info.major == 2) and not use_python3) or \
324 ((sys.version_info.major == 3) and use_python3)
325
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000326# Top level object so multiprocessing can pickle
327# Public access through OutputApi object.
328class _PresubmitResult(object):
329 """Base class for result objects."""
330 fatal = False
331 should_prompt = False
332
333 def __init__(self, message, items=None, long_text=''):
334 """
335 message: A short one-line message to indicate errors.
336 items: A list of short strings to indicate where errors occurred.
337 long_text: multi-line text output, e.g. from another tool
338 """
Bruce Dawsondb8622b2022-04-03 15:38:12 +0000339 self._message = _PresubmitResult._ensure_str(message)
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000340 self._items = items or []
Tom McKee61c72652021-07-20 11:56:32 +0000341 self._long_text = _PresubmitResult._ensure_str(long_text.rstrip())
Bruce Dawsondca14bc2022-09-15 20:59:38 +0000342 if _SHOW_CALLSTACKS:
343 self._long_text += 'Presubmit result call stack is:\n'
344 self._long_text += ''.join(traceback.format_stack(None, 8))
Tom McKee61c72652021-07-20 11:56:32 +0000345
346 @staticmethod
347 def _ensure_str(val):
348 """
349 val: A "stringish" value. Can be any of str, unicode or bytes.
350 returns: A str after applying encoding/decoding as needed.
351 Assumes/uses UTF-8 for relevant inputs/outputs.
352
353 We'd prefer to use six.ensure_str but our copy of six is old :(
354 """
355 if isinstance(val, str):
356 return val
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000357
Tom McKee61c72652021-07-20 11:56:32 +0000358 if six.PY2 and isinstance(val, unicode):
359 return val.encode()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000360
361 if six.PY3 and isinstance(val, bytes):
Tom McKee61c72652021-07-20 11:56:32 +0000362 return val.decode()
363 raise ValueError("Unknown string type %s" % type(val))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000364
Edward Lemur6eb1d322020-02-27 22:20:15 +0000365 def handle(self):
366 sys.stdout.write(self._message)
367 sys.stdout.write('\n')
Takuto Ikutabaa7be02022-08-23 00:19:34 +0000368 for item in self._items:
Edward Lemur6eb1d322020-02-27 22:20:15 +0000369 sys.stdout.write(' ')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000370 # Write separately in case it's unicode.
Edward Lemur6eb1d322020-02-27 22:20:15 +0000371 sys.stdout.write(str(item))
Edward Lemur6eb1d322020-02-27 22:20:15 +0000372 sys.stdout.write('\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000373 if self._long_text:
Edward Lemur6eb1d322020-02-27 22:20:15 +0000374 sys.stdout.write('\n***************\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000375 # Write separately in case it's unicode.
Tom McKee61c72652021-07-20 11:56:32 +0000376 sys.stdout.write(self._long_text)
Edward Lemur6eb1d322020-02-27 22:20:15 +0000377 sys.stdout.write('\n***************\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000378
Debrian Figueroadd2737e2019-06-21 23:50:13 +0000379 def json_format(self):
380 return {
381 'message': self._message,
Debrian Figueroa6095d402019-06-28 18:47:18 +0000382 'items': [str(item) for item in self._items],
Debrian Figueroadd2737e2019-06-21 23:50:13 +0000383 'long_text': self._long_text,
384 'fatal': self.fatal
385 }
386
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000387
388# Top level object so multiprocessing can pickle
389# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000390class _PresubmitError(_PresubmitResult):
391 """A hard presubmit error."""
392 fatal = True
393
394
395# Top level object so multiprocessing can pickle
396# Public access through OutputApi object.
397class _PresubmitPromptWarning(_PresubmitResult):
398 """An warning that prompts the user if they want to continue."""
399 should_prompt = True
400
401
402# Top level object so multiprocessing can pickle
403# Public access through OutputApi object.
404class _PresubmitNotifyResult(_PresubmitResult):
405 """Just print something to the screen -- but it's not even a warning."""
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000406
407
408# Top level object so multiprocessing can pickle
409# Public access through OutputApi object.
410class _MailTextResult(_PresubmitResult):
411 """A warning that should be included in the review request email."""
412 def __init__(self, *args, **kwargs):
413 super(_MailTextResult, self).__init__()
414 raise NotImplementedError()
415
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000416class GerritAccessor(object):
417 """Limited Gerrit functionality for canned presubmit checks to work.
418
419 To avoid excessive Gerrit calls, caches the results.
420 """
421
Edward Lesmeseb1bd622021-03-01 19:54:07 +0000422 def __init__(self, url=None, project=None, branch=None):
423 self.host = urlparse.urlparse(url).netloc if url else None
424 self.project = project
425 self.branch = branch
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000426 self.cache = {}
Edward Lesmes8170c292021-03-19 20:04:43 +0000427 self.code_owners_enabled = None
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000428
429 def _FetchChangeDetail(self, issue):
430 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100431 try:
432 return gerrit_util.GetChangeDetail(
433 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700434 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100435 except gerrit_util.GerritError as e:
436 if e.http_status == 404:
437 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
438 'no credentials to fetch issue details' % issue)
439 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000440
441 def GetChangeInfo(self, issue):
442 """Returns labels and all revisions (patchsets) for this issue.
443
444 The result is a dictionary according to Gerrit REST Api.
445 https://gerrit-review.googlesource.com/Documentation/rest-api.html
446
447 However, API isn't very clear what's inside, so see tests for example.
448 """
449 assert issue
450 cache_key = int(issue)
451 if cache_key not in self.cache:
452 self.cache[cache_key] = self._FetchChangeDetail(issue)
453 return self.cache[cache_key]
454
455 def GetChangeDescription(self, issue, patchset=None):
456 """If patchset is none, fetches current patchset."""
457 info = self.GetChangeInfo(issue)
458 # info is a reference to cache. We'll modify it here adding description to
459 # it to the right patchset, if it is not yet there.
460
461 # Find revision info for the patchset we want.
462 if patchset is not None:
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000463 for rev, rev_info in info['revisions'].items():
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000464 if str(rev_info['_number']) == str(patchset):
465 break
466 else:
467 raise Exception('patchset %s doesn\'t exist in issue %s' % (
468 patchset, issue))
469 else:
470 rev = info['current_revision']
471 rev_info = info['revisions'][rev]
472
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100473 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000474
Mun Yong Jang603d01e2017-12-19 16:38:30 -0800475 def GetDestRef(self, issue):
476 ref = self.GetChangeInfo(issue)['branch']
477 if not ref.startswith('refs/'):
478 # NOTE: it is possible to create 'refs/x' branch,
479 # aka 'refs/heads/refs/x'. However, this is ill-advised.
480 ref = 'refs/heads/%s' % ref
481 return ref
482
Edward Lesmes02d4b822020-11-11 00:37:35 +0000483 def _GetApproversForLabel(self, issue, label):
484 change_info = self.GetChangeInfo(issue)
485 label_info = change_info.get('labels', {}).get(label, {})
486 values = label_info.get('values', {}).keys()
487 if not values:
488 return []
489 max_value = max(int(v) for v in values)
490 return [v for v in label_info.get('all', [])
491 if v.get('value', 0) == max_value]
492
Edward Lesmesc4566172021-03-19 16:55:13 +0000493 def IsBotCommitApproved(self, issue):
494 return bool(self._GetApproversForLabel(issue, 'Bot-Commit'))
495
Edward Lesmescf49cb82020-11-11 01:08:36 +0000496 def IsOwnersOverrideApproved(self, issue):
497 return bool(self._GetApproversForLabel(issue, 'Owners-Override'))
498
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000499 def GetChangeOwner(self, issue):
500 return self.GetChangeInfo(issue)['owner']['email']
501
502 def GetChangeReviewers(self, issue, approving_only=True):
Aaron Gable8b478f02017-07-31 15:33:19 -0700503 changeinfo = self.GetChangeInfo(issue)
504 if approving_only:
Edward Lesmes02d4b822020-11-11 00:37:35 +0000505 reviewers = self._GetApproversForLabel(issue, 'Code-Review')
Aaron Gable8b478f02017-07-31 15:33:19 -0700506 else:
507 reviewers = changeinfo.get('reviewers', {}).get('REVIEWER', [])
508 return [r.get('email') for r in reviewers]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000509
Edward Lemure4d329c2020-02-03 20:41:18 +0000510 def UpdateDescription(self, description, issue):
511 gerrit_util.SetCommitMessage(self.host, issue, description, notify='NONE')
512
Edward Lesmes8170c292021-03-19 20:04:43 +0000513 def IsCodeOwnersEnabledOnRepo(self):
514 if self.code_owners_enabled is None:
515 self.code_owners_enabled = gerrit_util.IsCodeOwnersEnabledOnRepo(
Edward Lesmes392c4072021-03-19 21:58:45 +0000516 self.host, self.project)
Edward Lesmes8170c292021-03-19 20:04:43 +0000517 return self.code_owners_enabled
518
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000519
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000520class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000521 """An instance of OutputApi gets passed to presubmit scripts so that they
522 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000523 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000524 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000525 PresubmitError = _PresubmitError
526 PresubmitPromptWarning = _PresubmitPromptWarning
527 PresubmitNotifyResult = _PresubmitNotifyResult
528 MailTextResult = _MailTextResult
529
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000530 def __init__(self, is_committing):
531 self.is_committing = is_committing
Daniel Cheng7227d212017-11-17 08:12:37 -0800532 self.more_cc = []
533
534 def AppendCC(self, cc):
535 """Appends a user to cc for this change."""
Daniel Cheng0e9f6682022-10-19 17:42:57 +0000536 if cc not in self.more_cc:
537 self.more_cc.append(cc)
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000538
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000539 def PresubmitPromptOrNotify(self, *args, **kwargs):
540 """Warn the user when uploading, but only notify if committing."""
541 if self.is_committing:
542 return self.PresubmitNotifyResult(*args, **kwargs)
543 return self.PresubmitPromptWarning(*args, **kwargs)
544
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000545
546class InputApi(object):
547 """An instance of this object is passed to presubmit scripts so they can
548 know stuff about the change they're looking at.
549 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000550 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800551 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000552
maruel@chromium.org3410d912009-06-09 20:56:16 +0000553 # File extensions that are considered source files from a style guide
554 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000555 #
556 # Files without an extension aren't included in the list. If you want to
local_bot30f774e2020-06-25 18:23:34 +0000557 # filter them as source files, add r'(^|.*?[\\\/])[^.]+$' to the allow list.
local_bot64021412020-07-08 21:05:39 +0000558 # Note that ALL CAPS files are skipped in DEFAULT_FILES_TO_SKIP below.
559 DEFAULT_FILES_TO_CHECK = (
maruel@chromium.org3410d912009-06-09 20:56:16 +0000560 # C++ and friends
Edward Lemura5799e32020-01-17 19:26:51 +0000561 r'.+\.c$', r'.+\.cc$', r'.+\.cpp$', r'.+\.h$', r'.+\.m$', r'.+\.mm$',
562 r'.+\.inl$', r'.+\.asm$', r'.+\.hxx$', r'.+\.hpp$', r'.+\.s$', r'.+\.S$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000563 # Scripts
dpapad443d9132022-05-05 00:17:30 +0000564 r'.+\.js$', r'.+\.ts$', r'.+\.py$', r'.+\.sh$', r'.+\.rb$', r'.+\.pl$',
565 r'.+\.pm$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000566 # Other
Edward Lemura5799e32020-01-17 19:26:51 +0000567 r'.+\.java$', r'.+\.mk$', r'.+\.am$', r'.+\.css$', r'.+\.mojom$',
Bruce Dawson7a81ebf2023-01-03 18:36:18 +0000568 r'.+\.fidl$', r'.+\.rs$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000569 )
570
571 # Path regexp that should be excluded from being considered containing source
572 # files. Don't modify this list from a presubmit script!
local_bot64021412020-07-08 21:05:39 +0000573 DEFAULT_FILES_TO_SKIP = (
Edward Lemura5799e32020-01-17 19:26:51 +0000574 r'testing_support[\\\/]google_appengine[\\\/].*',
575 r'.*\bexperimental[\\\/].*',
Kent Tamura179dd1e2018-04-26 15:07:41 +0900576 # Exclude third_party/.* but NOT third_party/{WebKit,blink}
577 # (crbug.com/539768 and crbug.com/836555).
Edward Lemura5799e32020-01-17 19:26:51 +0000578 r'.*\bthird_party[\\\/](?!(WebKit|blink)[\\\/]).*',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000579 # Output directories (just in case)
Edward Lemura5799e32020-01-17 19:26:51 +0000580 r'.*\bDebug[\\\/].*',
581 r'.*\bRelease[\\\/].*',
582 r'.*\bxcodebuild[\\\/].*',
583 r'.*\bout[\\\/].*',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000584 # All caps files like README and LICENCE.
Edward Lemura5799e32020-01-17 19:26:51 +0000585 r'.*\b[A-Z0-9_]{2,}$',
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000586 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
Edward Lemura5799e32020-01-17 19:26:51 +0000587 r'(|.*[\\\/])\.git[\\\/].*',
588 r'(|.*[\\\/])\.svn[\\\/].*',
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000589 # There is no point in processing a patch file.
Edward Lemura5799e32020-01-17 19:26:51 +0000590 r'.+\.diff$',
591 r'.+\.patch$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000592 )
593
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000594 def __init__(self, change, presubmit_path, is_committing,
Bruce Dawson09c0c072022-05-26 20:28:58 +0000595 verbose, gerrit_obj, dry_run=None, thread_pool=None, parallel=False,
596 no_diffs=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000597 """Builds an InputApi object.
598
599 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000600 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000601 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000602 is_committing: True if the change is about to be committed.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000603 gerrit_obj: provides basic Gerrit codereview functionality.
604 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -0400605 parallel: if true, all tests reported via input_api.RunTests for all
606 PRESUBMIT files will be run in parallel.
Bruce Dawson09c0c072022-05-26 20:28:58 +0000607 no_diffs: if true, implies that --files or --all was specified so some
608 checks can be skipped, and some errors will be messages.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000609 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000610 # Version number of the presubmit_support script.
611 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000612 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000613 self.is_committing = is_committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000614 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000615 self.dry_run = dry_run
Bruce Dawson09c0c072022-05-26 20:28:58 +0000616 self.no_diffs = no_diffs
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000617
Edward Lesmes8e282792018-04-03 18:50:29 -0400618 self.parallel = parallel
619 self.thread_pool = thread_pool or ThreadPool()
620
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000621 # We expose various modules and functions as attributes of the input_api
622 # so that presubmit scripts don't have to import them.
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +0900623 self.ast = ast
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000624 self.basename = os.path.basename
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000625 self.cpplint = cpplint
dcheng091b7db2016-06-16 01:27:51 -0700626 self.fnmatch = fnmatch
Yoshisato Yanagisawa04600b42019-03-15 03:03:41 +0000627 self.gclient_paths = gclient_paths
Yoshisato Yanagisawa57dd17b2019-03-22 09:10:29 +0000628 # TODO(yyanagisawa): stop exposing this when python3 become default.
629 # Since python3's tempfile has TemporaryDirectory, we do not need this.
630 self.temporary_directory = gclient_utils.temporary_directory
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000631 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000632 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000633 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000634 self.os_listdir = os.listdir
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000635 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000636 self.os_stat = os.stat
Yoshisato Yanagisawa406de132018-06-29 05:43:25 +0000637 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000638 self.re = re
639 self.subprocess = subprocess
Edward Lemurb9830242019-10-30 22:19:20 +0000640 self.sys = sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000641 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000642 self.time = time
maruel@chromium.org1487d532009-06-06 00:22:57 +0000643 self.unittest = unittest
Edward Lemurb9830242019-10-30 22:19:20 +0000644 if sys.version_info.major == 2:
645 self.urllib2 = urllib2
Edward Lemur16af3562019-10-17 22:11:33 +0000646 self.urllib_request = urllib_request
647 self.urllib_error = urllib_error
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000648
Robert Iannucci50258932018-03-19 10:30:59 -0700649 self.is_windows = sys.platform == 'win32'
650
Edward Lemurb9646622019-10-25 20:57:35 +0000651 # Set python_executable to 'vpython' in order to allow scripts in other
652 # repos (e.g. src.git) to automatically pick up that repo's .vpython file,
653 # instead of inheriting the one in depot_tools.
654 self.python_executable = 'vpython'
Erik Staab69135d12021-05-14 22:31:57 +0000655 # Offer a python 3 executable for use during the migration off of python 2.
656 self.python3_executable = 'vpython3'
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000657 self.environ = os.environ
658
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000659 # InputApi.platform is the platform you're currently running on.
660 self.platform = sys.platform
661
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000662 self.cpu_count = multiprocessing.cpu_count()
Bruce Dawson8254f062022-06-17 17:33:08 +0000663 if self.is_windows:
664 # TODO(crbug.com/1190269) - we can't use more than 56 child processes on
665 # Windows or Python3 may hang.
666 self.cpu_count = min(self.cpu_count, 56)
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000667
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000668 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000669 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000670
671 # We carry the canned checks so presubmit scripts can easily use them.
672 self.canned_checks = presubmit_canned_checks
673
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100674 # Temporary files we must manually remove at the end of a run.
675 self._named_temporary_files = []
Jochen Eisinger72606f82017-04-04 10:44:18 +0200676
Edward Lesmesdc11c6b2021-03-19 19:22:05 +0000677 self.owners_client = None
Bruce Dawsoneb8426e2022-08-05 23:58:15 +0000678 if self.gerrit and not 'PRESUBMIT_SKIP_NETWORK' in self.environ:
Bruce Dawson9b9f4512022-04-27 00:56:52 +0000679 try:
680 self.owners_client = owners_client.GetCodeOwnersClient(
Bruce Dawson9b9f4512022-04-27 00:56:52 +0000681 host=self.gerrit.host,
682 project=self.gerrit.project,
683 branch=self.gerrit.branch)
684 except Exception as e:
685 print('Failed to set owners_client - %s' % str(e))
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200686 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000687 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000688 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000689
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000690 # Replace <hash_map> and <hash_set> as headers that need to be included
Edward Lemura5799e32020-01-17 19:26:51 +0000691 # with 'base/containers/hash_tables.h' instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000692 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800693 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000694 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000695 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000696 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
697 for (a, b, header) in cpplint._re_pattern_templates
698 ]
699
Edward Lemurecc27072020-01-06 16:42:34 +0000700 def SetTimeout(self, timeout):
701 self.thread_pool.timeout = timeout
702
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000703 def PresubmitLocalPath(self):
704 """Returns the local path of the presubmit script currently being run.
705
706 This is useful if you don't want to hard-code absolute paths in the
707 presubmit script. For example, It can be used to find another file
708 relative to the PRESUBMIT.py script, so the whole tree can be branched and
709 the presubmit script still works, without editing its content.
710 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000711 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000712
agable0b65e732016-11-22 09:25:46 -0800713 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000714 """Same as input_api.change.AffectedFiles() except only lists files
715 (and optionally directories) in the same directory as the current presubmit
Bruce Dawson7a0b07a2020-04-23 17:14:40 +0000716 script, or subdirectories thereof. Note that files are listed using the OS
717 path separator, so backslashes are used as separators on Windows.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000718 """
Josip Sokcevic512fd3b2023-03-01 21:06:41 +0000719 dir_with_slash = utils.normpath(self.PresubmitLocalPath())
Bruce Dawson31bfd512022-05-10 23:19:39 +0000720 # normpath strips trailing path separators, so the trailing separator has to
721 # be added after the normpath call.
722 if len(dir_with_slash) > 0:
723 dir_with_slash += os.path.sep
sail@chromium.org5538e022011-05-12 17:53:16 +0000724
Josip Sokcevic512fd3b2023-03-01 21:06:41 +0000725 return list(
726 filter(
727 lambda x: utils.normpath(x.AbsoluteLocalPath()).startswith(
728 dir_with_slash),
729 self.change.AffectedFiles(include_deletes, file_filter)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000730
agable0b65e732016-11-22 09:25:46 -0800731 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000732 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800733 paths = [af.LocalPath() for af in self.AffectedFiles()]
Edward Lemura5799e32020-01-17 19:26:51 +0000734 logging.debug('LocalPaths: %s', paths)
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000735 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000736
agable0b65e732016-11-22 09:25:46 -0800737 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000738 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800739 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000740
John Budorick16162372018-04-18 10:39:53 -0700741 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
agable0b65e732016-11-22 09:25:46 -0800742 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000743 in the same directory as the current presubmit script, or subdirectories
744 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000745 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000746 if include_deletes is not None:
Edward Lemura5799e32020-01-17 19:26:51 +0000747 warn('AffectedTestableFiles(include_deletes=%s)'
748 ' is deprecated and ignored' % str(include_deletes),
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000749 category=DeprecationWarning,
750 stacklevel=2)
Josip Sokcevic4de5dea2022-03-23 21:15:14 +0000751 # pylint: disable=consider-using-generator
752 return [
753 x for x in self.AffectedFiles(include_deletes=False, **kwargs)
754 if x.IsTestableFile()
755 ]
agable0b65e732016-11-22 09:25:46 -0800756
757 def AffectedTextFiles(self, include_deletes=None):
758 """An alias to AffectedTestableFiles for backwards compatibility."""
759 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000760
Josip Sokcevic8c955952021-02-01 21:32:57 +0000761 def FilterSourceFile(self,
762 affected_file,
763 files_to_check=None,
764 files_to_skip=None,
765 allow_list=None,
766 block_list=None):
Edward Lemura5799e32020-01-17 19:26:51 +0000767 """Filters out files that aren't considered 'source file'.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000768
local_bot64021412020-07-08 21:05:39 +0000769 If files_to_check or files_to_skip is None, InputApi.DEFAULT_FILES_TO_CHECK
770 and InputApi.DEFAULT_FILES_TO_SKIP is used respectively.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000771
Bruce Dawson635383f2022-09-13 16:23:18 +0000772 affected_file.LocalPath() needs to re.match an entry in the files_to_check
773 list and not re.match any entries in the files_to_skip list.
774 '/' path separators should be used in the regular expressions and will work
775 on Windows as well as other platforms.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000776
777 Note: Copy-paste this function to suit your needs or use a lambda function.
778 """
local_bot64021412020-07-08 21:05:39 +0000779 if files_to_check is None:
780 files_to_check = self.DEFAULT_FILES_TO_CHECK
781 if files_to_skip is None:
782 files_to_skip = self.DEFAULT_FILES_TO_SKIP
local_bot30f774e2020-06-25 18:23:34 +0000783
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000784 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000785 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000786 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000787 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000788 return True
Bruce Dawsona3a014e2022-04-27 23:28:17 +0000789 # Handle the cases where the files regex only handles /, but the local
790 # path uses \.
791 if self.is_windows and self.re.match(item, local_path.replace(
792 '\\', '/')):
793 return True
maruel@chromium.org3410d912009-06-09 20:56:16 +0000794 return False
local_bot64021412020-07-08 21:05:39 +0000795 return (Find(affected_file, files_to_check) and
796 not Find(affected_file, files_to_skip))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000797
798 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800799 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000800
801 If source_file is None, InputApi.FilterSourceFile() is used.
802 """
803 if not source_file:
804 source_file = self.FilterSourceFile
Edward Lemurb9830242019-10-30 22:19:20 +0000805 return list(filter(source_file, self.AffectedTestableFiles()))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000806
807 def RightHandSideLines(self, source_file_filter=None):
Edward Lemura5799e32020-01-17 19:26:51 +0000808 """An iterator over all text lines in 'new' version of changed files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000809
810 Only lists lines from new or modified text files in the change that are
811 contained by the directory of the currently executing presubmit script.
812
813 This is useful for doing line-by-line regex checks, like checking for
814 trailing whitespace.
815
816 Yields:
817 a 3 tuple:
Josip Sokcevic688adfe2023-03-01 22:46:12 +0000818 the change.AffectedFile instance of the current file;
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000819 integer line number (1-based); and
820 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000821
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000822 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000823 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000824 files = self.AffectedSourceFiles(source_file_filter)
Josip Sokcevic688adfe2023-03-01 22:46:12 +0000825 return libchange.RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000826
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000827 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000828 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000829
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000830 Deny reading anything outside the repository.
831 """
Josip Sokcevic688adfe2023-03-01 22:46:12 +0000832 if isinstance(file_item, libchange.AffectedFile):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000833 file_item = file_item.AbsoluteLocalPath()
834 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000835 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000836 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000837
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100838 def CreateTemporaryFile(self, **kwargs):
839 """Returns a named temporary file that must be removed with a call to
840 RemoveTemporaryFiles().
841
842 All keyword arguments are forwarded to tempfile.NamedTemporaryFile(),
843 except for |delete|, which is always set to False.
844
845 Presubmit checks that need to create a temporary file and pass it for
846 reading should use this function instead of NamedTemporaryFile(), as
847 Windows fails to open a file that is already open for writing.
848
849 with input_api.CreateTemporaryFile() as f:
850 f.write('xyz')
851 f.close()
852 input_api.subprocess.check_output(['script-that', '--reads-from',
853 f.name])
854
855
856 Note that callers of CreateTemporaryFile() should not worry about removing
857 any temporary file; this is done transparently by the presubmit handling
858 code.
859 """
860 if 'delete' in kwargs:
861 # Prevent users from passing |delete|; we take care of file deletion
862 # ourselves and this prevents unintuitive error messages when we pass
863 # delete=False and 'delete' is also in kwargs.
864 raise TypeError('CreateTemporaryFile() does not take a "delete" '
865 'argument, file deletion is handled automatically by '
866 'the same presubmit_support code that creates InputApi '
867 'objects.')
868 temp_file = self.tempfile.NamedTemporaryFile(delete=False, **kwargs)
869 self._named_temporary_files.append(temp_file.name)
870 return temp_file
871
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000872 @property
873 def tbr(self):
874 """Returns if a change is TBR'ed."""
Jeremy Romandce22502017-06-20 15:37:29 -0400875 return 'TBR' in self.change.tags or self.change.TBRsFromDescription()
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000876
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000877 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000878 tests = []
879 msgs = []
880 for t in tests_mix:
Edward Lesmes8e282792018-04-03 18:50:29 -0400881 if isinstance(t, OutputApi.PresubmitResult) and t:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000882 msgs.append(t)
883 else:
884 assert issubclass(t.message, _PresubmitResult)
885 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000886 if self.verbose:
887 t.info = _PresubmitNotifyResult
Edward Lemur1037c742018-05-01 18:56:04 -0400888 if not t.kwargs.get('cwd'):
889 t.kwargs['cwd'] = self.PresubmitLocalPath()
Edward Lesmes8e282792018-04-03 18:50:29 -0400890 self.thread_pool.AddTests(tests, parallel)
Edward Lemur21000eb2019-05-24 23:25:58 +0000891 # When self.parallel is True (i.e. --parallel is passed as an option)
892 # RunTests doesn't actually run tests. It adds them to a ThreadPool that
893 # will run all tests once all PRESUBMIT files are processed.
894 # Otherwise, it will run them and return the results.
895 if not self.parallel:
Edward Lesmes8e282792018-04-03 18:50:29 -0400896 msgs.extend(self.thread_pool.RunAsync())
897 return msgs
scottmg86099d72016-09-01 09:16:51 -0700898
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000899
rmistry@google.com5626a922015-02-26 14:03:30 +0000900class GetPostUploadExecuter(object):
Pavol Marko624e7ee2023-01-09 09:56:29 +0000901 def __init__(self, change, gerrit_obj, use_python3):
Josip Sokcevice293d3d2022-02-16 22:52:15 +0000902 """
903 Args:
Pavol Marko624e7ee2023-01-09 09:56:29 +0000904 change: The Change object.
905 gerrit_obj: provides basic Gerrit codereview functionality.
Josip Sokcevice293d3d2022-02-16 22:52:15 +0000906 use_python3: if true, will use python3 instead of python2 by default
907 if USE_PYTHON3 is not specified.
908 """
Pavol Marko624e7ee2023-01-09 09:56:29 +0000909 self.change = change
910 self.gerrit = gerrit_obj
Josip Sokcevice293d3d2022-02-16 22:52:15 +0000911 self.use_python3 = use_python3
912
Pavol Marko624e7ee2023-01-09 09:56:29 +0000913 def ExecPresubmitScript(self, script_text, presubmit_path):
rmistry@google.com5626a922015-02-26 14:03:30 +0000914 """Executes PostUploadHook() from a single presubmit script.
Josip Sokcevic632bbc02022-05-19 05:32:50 +0000915 Caller is responsible for validating whether the hook should be executed
916 and should only call this function if it should be.
rmistry@google.com5626a922015-02-26 14:03:30 +0000917
918 Args:
919 script_text: The text of the presubmit script.
920 presubmit_path: Project script to run.
rmistry@google.com5626a922015-02-26 14:03:30 +0000921
922 Return:
923 A list of results objects.
924 """
Pavol Marko624e7ee2023-01-09 09:56:29 +0000925 # Change to the presubmit file's directory to support local imports.
926 presubmit_dir = os.path.dirname(presubmit_path)
927 main_path = os.getcwd()
928 try:
929 os.chdir(presubmit_dir)
930 return self._execute_with_local_working_directory(script_text,
931 presubmit_dir,
932 presubmit_path)
933 finally:
934 # Return the process to the original working directory.
935 os.chdir(main_path)
936
937 def _execute_with_local_working_directory(self, script_text, presubmit_dir,
938 presubmit_path):
rmistry@google.com5626a922015-02-26 14:03:30 +0000939 context = {}
940 try:
Pavol Marko624e7ee2023-01-09 09:56:29 +0000941 exec(compile(script_text, presubmit_path, 'exec', dont_inherit=True),
Raul Tambre09e64b42019-05-14 01:57:22 +0000942 context)
Raul Tambre7c938462019-05-24 16:35:35 +0000943 except Exception as e:
rmistry@google.com5626a922015-02-26 14:03:30 +0000944 raise PresubmitFailure('"%s" had an exception.\n%s'
945 % (presubmit_path, e))
946
947 function_name = 'PostUploadHook'
948 if function_name not in context:
949 return {}
950 post_upload_hook = context[function_name]
951 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
952 raise PresubmitFailure(
953 'Expected function "PostUploadHook" to take three arguments.')
Pavol Marko624e7ee2023-01-09 09:56:29 +0000954 return post_upload_hook(self.gerrit, self.change, OutputApi(False))
rmistry@google.com5626a922015-02-26 14:03:30 +0000955
956
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +0000957def _MergeMasters(masters1, masters2):
958 """Merges two master maps. Merges also the tests of each builder."""
959 result = {}
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000960 for (master, builders) in itertools.chain(masters1.items(),
961 masters2.items()):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +0000962 new_builders = result.setdefault(master, {})
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000963 for (builder, tests) in builders.items():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +0000964 new_builders.setdefault(builder, set([])).update(tests)
965 return result
966
967
Josip Sokcevice293d3d2022-02-16 22:52:15 +0000968def DoPostUploadExecuter(change, gerrit_obj, verbose, use_python3=False):
rmistry@google.com5626a922015-02-26 14:03:30 +0000969 """Execute the post upload hook.
970
971 Args:
972 change: The Change object.
Edward Lemur016a0872020-02-04 22:13:28 +0000973 gerrit_obj: The GerritAccessor object.
rmistry@google.com5626a922015-02-26 14:03:30 +0000974 verbose: Prints debug info.
Josip Sokcevice293d3d2022-02-16 22:52:15 +0000975 use_python3: if true, default to using Python3 for presubmit checks
976 rather than Python2.
rmistry@google.com5626a922015-02-26 14:03:30 +0000977 """
Josip Sokcevice293d3d2022-02-16 22:52:15 +0000978 python_version = 'Python %s' % sys.version_info.major
979 sys.stdout.write('Running %s post upload checks ...\n' % python_version)
Josip Sokcevic512fd3b2023-03-01 21:06:41 +0000980 presubmit_files = utils.ListRelevantFilesInSourceCheckout(
981 change.LocalPaths(), change.RepositoryRoot(), _PRESUBMIT_FILE_REGEX,
982 _PRESUBMIT_FILE_EXCLUDE)
rmistry@google.com5626a922015-02-26 14:03:30 +0000983 if not presubmit_files and verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +0000984 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
rmistry@google.com5626a922015-02-26 14:03:30 +0000985 results = []
Pavol Marko624e7ee2023-01-09 09:56:29 +0000986 executer = GetPostUploadExecuter(change, gerrit_obj, use_python3)
rmistry@google.com5626a922015-02-26 14:03:30 +0000987 # The root presubmit file should be executed after the ones in subdirectories.
988 # i.e. the specific post upload hooks should run before the general ones.
Josip Sokcevic512fd3b2023-03-01 21:06:41 +0000989 # Thus, reverse the order.
rmistry@google.com5626a922015-02-26 14:03:30 +0000990 presubmit_files.reverse()
991
992 for filename in presubmit_files:
993 filename = os.path.abspath(filename)
rmistry@google.com5626a922015-02-26 14:03:30 +0000994 # Accept CRLF presubmit script.
Bruce Dawson6e70e862022-07-01 19:37:01 +0000995 presubmit_script = gclient_utils.FileRead(filename).replace('\r\n', '\n')
Josip Sokcevic632bbc02022-05-19 05:32:50 +0000996 if _ShouldRunPresubmit(presubmit_script, use_python3):
Bruce Dawson8d78bd12022-08-12 19:17:03 +0000997 if sys.version_info[0] == 2:
Bruce Dawson443f9852022-09-08 17:30:59 +0000998 sys.stdout.write(
999 'Running %s under Python 2. Add USE_PYTHON3 = True to prevent '
1000 'this.\n' % filename)
Bruce Dawson8d78bd12022-08-12 19:17:03 +00001001 elif verbose:
1002 sys.stdout.write('Running %s\n' % filename)
Pavol Marko624e7ee2023-01-09 09:56:29 +00001003 results.extend(executer.ExecPresubmitScript(presubmit_script, filename))
rmistry@google.com5626a922015-02-26 14:03:30 +00001004
Edward Lemur6eb1d322020-02-27 22:20:15 +00001005 if not results:
1006 return 0
1007
1008 sys.stdout.write('\n')
1009 sys.stdout.write('** Post Upload Hook Messages **\n')
1010
1011 exit_code = 0
1012 for result in results:
1013 if result.fatal:
1014 exit_code = 1
1015 result.handle()
1016 sys.stdout.write('\n')
1017
1018 return exit_code
rmistry@google.com5626a922015-02-26 14:03:30 +00001019
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001020class PresubmitExecuter(object):
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001021 def __init__(self, change, committing, verbose, gerrit_obj, dry_run=None,
Bruce Dawson09c0c072022-05-26 20:28:58 +00001022 thread_pool=None, parallel=False, use_python3=False,
1023 no_diffs=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024 """
1025 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001026 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001027 committing: True if 'git cl land' is running, False if 'git cl upload' is.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001028 gerrit_obj: provides basic Gerrit codereview functionality.
1029 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001030 parallel: if true, all tests reported via input_api.RunTests for all
1031 PRESUBMIT files will be run in parallel.
Dirk Pranke6f0df682021-06-25 00:42:33 +00001032 use_python3: if true, will use python3 instead of python2 by default
1033 if USE_PYTHON3 is not specified.
Bruce Dawson09c0c072022-05-26 20:28:58 +00001034 no_diffs: if true, implies that --files or --all was specified so some
1035 checks can be skipped, and some errors will be messages.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001036 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001037 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001038 self.committing = committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001039 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001040 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001041 self.dry_run = dry_run
Daniel Cheng7227d212017-11-17 08:12:37 -08001042 self.more_cc = []
Edward Lesmes8e282792018-04-03 18:50:29 -04001043 self.thread_pool = thread_pool
1044 self.parallel = parallel
Dirk Pranke6f0df682021-06-25 00:42:33 +00001045 self.use_python3 = use_python3
Bruce Dawson09c0c072022-05-26 20:28:58 +00001046 self.no_diffs = no_diffs
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001047
1048 def ExecPresubmitScript(self, script_text, presubmit_path):
1049 """Executes a single presubmit script.
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001050 Caller is responsible for validating whether the hook should be executed
1051 and should only call this function if it should be.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001052
1053 Args:
1054 script_text: The text of the presubmit script.
1055 presubmit_path: The path to the presubmit file (this will be reported via
1056 input_api.PresubmitLocalPath()).
1057
1058 Return:
1059 A list of result objects, empty if no problems.
1060 """
chase@chromium.org8e416c82009-10-06 04:30:44 +00001061 # Change to the presubmit file's directory to support local imports.
Saagar Sanghavi98b332f2020-07-31 17:19:15 +00001062 presubmit_dir = os.path.dirname(presubmit_path)
Pavol Marko624e7ee2023-01-09 09:56:29 +00001063 main_path = os.getcwd()
1064 try:
1065 os.chdir(presubmit_dir)
1066 return self._execute_with_local_working_directory(script_text,
1067 presubmit_dir,
1068 presubmit_path)
1069 finally:
1070 # Return the process to the original working directory.
1071 os.chdir(main_path)
chase@chromium.org8e416c82009-10-06 04:30:44 +00001072
Pavol Marko624e7ee2023-01-09 09:56:29 +00001073 def _execute_with_local_working_directory(self, script_text, presubmit_dir,
1074 presubmit_path):
chase@chromium.org8e416c82009-10-06 04:30:44 +00001075 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001076 input_api = InputApi(self.change, presubmit_path, self.committing,
Aaron Gable668c1d82018-04-03 10:19:16 -07001077 self.verbose, gerrit_obj=self.gerrit,
Edward Lesmes8e282792018-04-03 18:50:29 -04001078 dry_run=self.dry_run, thread_pool=self.thread_pool,
Bruce Dawson09c0c072022-05-26 20:28:58 +00001079 parallel=self.parallel, no_diffs=self.no_diffs)
Daniel Cheng7227d212017-11-17 08:12:37 -08001080 output_api = OutputApi(self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001081 context = {}
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001082
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001083 try:
Bruce Dawson0ba2fd42022-07-21 13:47:21 +00001084 exec(compile(script_text, presubmit_path, 'exec', dont_inherit=True),
Raul Tambre09e64b42019-05-14 01:57:22 +00001085 context)
Raul Tambre7c938462019-05-24 16:35:35 +00001086 except Exception as e:
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001087 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001088
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001089 context['__args'] = (input_api, output_api)
Ben Pastene8351dc12020-08-06 05:01:35 +00001090
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001091 # Get path of presubmit directory relative to repository root.
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001092 # Always use forward slashes, so that path is same in *nix and Windows
1093 root = input_api.change.RepositoryRoot()
1094 rel_path = os.path.relpath(presubmit_dir, root)
1095 rel_path = rel_path.replace(os.path.sep, '/')
Ben Pastene8351dc12020-08-06 05:01:35 +00001096
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001097 # Get the URL of git remote origin and use it to identify host and project
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001098 host = project = ''
1099 if self.gerrit:
1100 host = self.gerrit.host or ''
1101 project = self.gerrit.project or ''
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001102
1103 # Prefix for test names
1104 prefix = 'presubmit:%s/%s:%s/' % (host, project, rel_path)
1105
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001106 # Perform all the desired presubmit checks.
1107 results = []
Ben Pastene8351dc12020-08-06 05:01:35 +00001108
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001109 try:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001110 version = [
1111 int(x) for x in context.get('PRESUBMIT_VERSION', '0.0.0').split('.')
1112 ]
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001113
Scott Leecc2fe9b2020-11-19 19:38:06 +00001114 with rdb_wrapper.client(prefix) as sink:
1115 if version >= [2, 0, 0]:
Andrew Grievebdfb8f22022-02-02 21:56:47 +00001116 # Copy the keys to prevent "dictionary changed size during iteration"
1117 # exception if checks add globals to context. E.g. sometimes the
1118 # Python runtime will add __warningregistry__.
1119 for function_name in list(context.keys()):
Scott Leecc2fe9b2020-11-19 19:38:06 +00001120 if not function_name.startswith('Check'):
1121 continue
1122 if function_name.endswith('Commit') and not self.committing:
1123 continue
1124 if function_name.endswith('Upload') and self.committing:
1125 continue
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001126 logging.debug('Running %s in %s', function_name, presubmit_path)
1127 results.extend(
Bruce Dawson8fa42e22022-03-29 17:05:38 +00001128 self._run_check_function(function_name, context, sink,
1129 presubmit_path))
Scott Leecc2fe9b2020-11-19 19:38:06 +00001130 logging.debug('Running %s done.', function_name)
1131 self.more_cc.extend(output_api.more_cc)
1132
1133 else: # Old format
1134 if self.committing:
1135 function_name = 'CheckChangeOnCommit'
1136 else:
1137 function_name = 'CheckChangeOnUpload'
Andrew Grievebdfb8f22022-02-02 21:56:47 +00001138 if function_name in list(context.keys()):
Scott Leecc2fe9b2020-11-19 19:38:06 +00001139 logging.debug('Running %s in %s', function_name, presubmit_path)
1140 results.extend(
Bruce Dawson8fa42e22022-03-29 17:05:38 +00001141 self._run_check_function(function_name, context, sink,
1142 presubmit_path))
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001143 logging.debug('Running %s done.', function_name)
1144 self.more_cc.extend(output_api.more_cc)
1145
1146 finally:
1147 for f in input_api._named_temporary_files:
1148 os.remove(f)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001149
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001150 return results
1151
Bruce Dawson8fa42e22022-03-29 17:05:38 +00001152 def _run_check_function(self, function_name, context, sink, presubmit_path):
Scott Leecc2fe9b2020-11-19 19:38:06 +00001153 """Evaluates and returns the result of a given presubmit function.
1154
1155 If sink is given, the result of the presubmit function will be reported
1156 to the ResultSink.
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001157
1158 Args:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001159 function_name: the name of the presubmit function to evaluate
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001160 context: a context dictionary in which the function will be evaluated
Scott Leecc2fe9b2020-11-19 19:38:06 +00001161 sink: an instance of ResultSink. None, by default.
1162 Returns:
1163 the result of the presubmit function call.
1164 """
1165 start_time = time_time()
1166 try:
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001167 result = eval(function_name + '(*__args)', context)
1168 self._check_result_type(result)
Allen Webbfe7d7092021-05-18 02:05:49 +00001169 except Exception:
Bruce Dawson10a82862022-05-27 19:25:56 +00001170 _, e_value, _ = sys.exc_info()
1171 result = [
1172 OutputApi.PresubmitError(
1173 'Evaluation of %s failed: %s, %s' %
1174 (function_name, e_value, traceback.format_exc()))
1175 ]
Scott Leecc2fe9b2020-11-19 19:38:06 +00001176
Bruce Dawson2bdc49f2021-05-21 19:13:03 +00001177 elapsed_time = time_time() - start_time
1178 if elapsed_time > 10.0:
Bruce Dawson6757d462022-07-13 04:04:40 +00001179 sys.stdout.write('%6.1fs to run %s from %s.\n' %
1180 (elapsed_time, function_name, presubmit_path))
Scott Leecc2fe9b2020-11-19 19:38:06 +00001181 if sink:
Erik Staab9f38b632022-10-31 14:05:24 +00001182 failure_reason = None
Scott Leecc2fe9b2020-11-19 19:38:06 +00001183 status = rdb_wrapper.STATUS_PASS
1184 if any(r.fatal for r in result):
1185 status = rdb_wrapper.STATUS_FAIL
Erik Staab9f38b632022-10-31 14:05:24 +00001186 failure_reasons = []
1187 for r in result:
1188 fields = r.json_format()
1189 message = fields['message']
1190 items = '\n'.join(' %s' % item for item in fields['items'])
1191 failure_reasons.append('\n'.join([message, items]))
1192 if failure_reasons:
1193 failure_reason = '\n'.join(failure_reasons)
1194 sink.report(function_name, status, elapsed_time, failure_reason)
Scott Leecc2fe9b2020-11-19 19:38:06 +00001195
1196 return result
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001197
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001198 def _check_result_type(self, result):
1199 """Helper function which ensures result is a list, and all elements are
1200 instances of OutputApi.PresubmitResult"""
1201 if not isinstance(result, (tuple, list)):
1202 raise PresubmitFailure('Presubmit functions must return a tuple or list')
1203 if not all(isinstance(res, OutputApi.PresubmitResult) for res in result):
1204 raise PresubmitFailure(
1205 'All presubmit results must be of types derived from '
1206 'output_api.PresubmitResult')
1207
1208
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001209def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001210 committing,
1211 verbose,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001212 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001213 may_prompt,
Aaron Gable668c1d82018-04-03 10:19:16 -07001214 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04001215 dry_run=None,
Debrian Figueroadd2737e2019-06-21 23:50:13 +00001216 parallel=False,
Dirk Pranke6f0df682021-06-25 00:42:33 +00001217 json_output=None,
Bruce Dawson09c0c072022-05-26 20:28:58 +00001218 use_python3=False,
1219 no_diffs=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001220 """Runs all presubmit checks that apply to the files in the change.
1221
1222 This finds all PRESUBMIT.py files in directories enclosing the files in the
1223 change (up to the repository root) and calls the relevant entrypoint function
1224 depending on whether the change is being committed or uploaded.
1225
1226 Prints errors, warnings and notifications. Prompts the user for warnings
1227 when needed.
1228
1229 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001230 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001231 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001232 verbose: Prints debug info.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001233 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001234 may_prompt: Enable (y/n) questions on warning or error. If False,
1235 any questions are answered with yes by default.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001236 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001237 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001238 parallel: if true, all tests specified by input_api.RunTests in all
1239 PRESUBMIT files will be run in parallel.
Dirk Pranke6f0df682021-06-25 00:42:33 +00001240 use_python3: if true, default to using Python3 for presubmit checks
1241 rather than Python2.
Bruce Dawson09c0c072022-05-26 20:28:58 +00001242 no_diffs: if true, implies that --files or --all was specified so some
1243 checks can be skipped, and some errors will be messages.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001244 Return:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001245 1 if presubmit checks failed or 0 otherwise.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001246 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001247 old_environ = os.environ
1248 try:
1249 # Make sure python subprocesses won't generate .pyc files.
1250 os.environ = os.environ.copy()
Edward Lemurb9830242019-10-30 22:19:20 +00001251 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001252
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001253 python_version = 'Python %s' % sys.version_info.major
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001254 if committing:
Dirk Pranke6fc394f2021-05-26 23:25:14 +00001255 sys.stdout.write('Running %s presubmit commit checks ...\n' %
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001256 python_version)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001257 else:
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001258 sys.stdout.write('Running %s presubmit upload checks ...\n' %
1259 python_version)
Edward Lemurecc27072020-01-06 16:42:34 +00001260 start_time = time_time()
Josip Sokcevic512fd3b2023-03-01 21:06:41 +00001261 presubmit_files = utils.ListRelevantFilesInSourceCheckout(
1262 change.AbsoluteLocalPaths(), change.RepositoryRoot(),
1263 _PRESUBMIT_FILE_REGEX, _PRESUBMIT_FILE_EXCLUDE)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001264 if not presubmit_files and verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001265 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001266 results = []
Bruce Dawsonc9f904f2022-10-14 20:59:49 +00001267 if sys.platform == 'win32':
1268 temp = os.environ['TEMP']
1269 else:
1270 temp = '/tmp'
1271 python2_usage_log_file = os.path.join(temp, 'python2_usage.txt')
Bruce Dawsonf2b04212022-06-10 16:42:54 +00001272 if os.path.exists(python2_usage_log_file):
1273 os.remove(python2_usage_log_file)
Edward Lesmes8e282792018-04-03 18:50:29 -04001274 thread_pool = ThreadPool()
Edward Lemur7e3c67f2018-07-20 20:52:49 +00001275 executer = PresubmitExecuter(change, committing, verbose, gerrit_obj,
Bruce Dawson09c0c072022-05-26 20:28:58 +00001276 dry_run, thread_pool, parallel, use_python3,
1277 no_diffs)
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001278 skipped_count = 0;
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001279 if default_presubmit:
1280 if verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001281 sys.stdout.write('Running default presubmit script.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001282 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001283 if _ShouldRunPresubmit(default_presubmit, use_python3):
1284 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1285 else:
1286 skipped_count += 1
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001287 for filename in presubmit_files:
1288 filename = os.path.abspath(filename)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001289 # Accept CRLF presubmit script.
Bruce Dawson6e70e862022-07-01 19:37:01 +00001290 presubmit_script = gclient_utils.FileRead(filename).replace('\r\n', '\n')
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001291 if _ShouldRunPresubmit(presubmit_script, use_python3):
Bruce Dawson8d78bd12022-08-12 19:17:03 +00001292 if sys.version_info[0] == 2:
Bruce Dawson443f9852022-09-08 17:30:59 +00001293 sys.stdout.write(
1294 'Running %s under Python 2. Add USE_PYTHON3 = True to prevent '
1295 'this.\n' % filename)
Bruce Dawson8d78bd12022-08-12 19:17:03 +00001296 elif verbose:
1297 sys.stdout.write('Running %s\n' % filename)
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001298 results += executer.ExecPresubmitScript(presubmit_script, filename)
1299 else:
1300 skipped_count += 1
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001301
Edward Lesmes8e282792018-04-03 18:50:29 -04001302 results += thread_pool.RunAsync()
1303
Bruce Dawsonf2b04212022-06-10 16:42:54 +00001304 if os.path.exists(python2_usage_log_file):
1305 with open(python2_usage_log_file) as f:
1306 python2_usage = [x.strip() for x in f.readlines()]
1307 results.append(
1308 OutputApi(committing).PresubmitPromptWarning(
1309 'Python 2 scripts were run during %s presubmits. Please see '
1310 'https://bugs.chromium.org/p/chromium/issues/detail?id=1313804'
1311 '#c61 for tips on resolving this.'
1312 % python_version,
1313 items=python2_usage))
1314
Edward Lemur6eb1d322020-02-27 22:20:15 +00001315 messages = {}
1316 should_prompt = False
1317 presubmits_failed = False
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001318 for result in results:
1319 if result.fatal:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001320 presubmits_failed = True
1321 messages.setdefault('ERRORS', []).append(result)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001322 elif result.should_prompt:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001323 should_prompt = True
1324 messages.setdefault('Warnings', []).append(result)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001325 else:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001326 messages.setdefault('Messages', []).append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001327
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001328 # Print the different message types in a consistent order. ERRORS go last
1329 # so that they will be most visible in the local-presubmit output.
1330 for name in ['Messages', 'Warnings', 'ERRORS']:
1331 if name in messages:
1332 items = messages[name]
Gavin Makd22bf602022-07-11 21:10:41 +00001333 sys.stdout.write('** Presubmit %s: %d **\n' % (name, len(items)))
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001334 for item in items:
1335 item.handle()
1336 sys.stdout.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001337
Edward Lemurecc27072020-01-06 16:42:34 +00001338 total_time = time_time() - start_time
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001339 if total_time > 1.0:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001340 sys.stdout.write(
Louis Romero4ca9e1c2022-03-10 10:29:11 +00001341 'Presubmit checks took %.1fs to calculate.\n' % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001342
Edward Lemur6eb1d322020-02-27 22:20:15 +00001343 if not should_prompt and not presubmits_failed:
Louis Romero4ca9e1c2022-03-10 10:29:11 +00001344 sys.stdout.write('%s presubmit checks passed.\n\n' % python_version)
Josip Sokcevic7592e0a2022-01-12 00:57:54 +00001345 elif should_prompt and not presubmits_failed:
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001346 sys.stdout.write('There were %s presubmit warnings. ' % python_version)
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001347 if may_prompt:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001348 presubmits_failed = not prompt_should_continue(
1349 'Are you sure you wish to continue? (y/N): ')
Garrett Beatyacb807e2020-08-28 18:17:24 +00001350 else:
1351 sys.stdout.write('\n')
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001352 else:
1353 sys.stdout.write('There were %s presubmit errors.\n' % python_version)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001354
Edward Lemur1dc66e12020-02-21 21:36:34 +00001355 if json_output:
1356 # Write the presubmit results to json output
1357 presubmit_results = {
1358 'errors': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001359 error.json_format()
1360 for error in messages.get('ERRORS', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001361 ],
1362 'notifications': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001363 notification.json_format()
1364 for notification in messages.get('Messages', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001365 ],
1366 'warnings': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001367 warning.json_format()
1368 for warning in messages.get('Warnings', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001369 ],
Edward Lemur6eb1d322020-02-27 22:20:15 +00001370 'more_cc': executer.more_cc,
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001371 'skipped_presubmits': skipped_count,
Edward Lemur1dc66e12020-02-21 21:36:34 +00001372 }
1373
1374 gclient_utils.FileWrite(
1375 json_output, json.dumps(presubmit_results, sort_keys=True))
1376
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001377 global _ASKED_FOR_FEEDBACK
1378 # Ask for feedback one time out of 5.
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001379 if (results and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
Edward Lemur6eb1d322020-02-27 22:20:15 +00001380 sys.stdout.write(
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001381 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1382 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1383 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001384 _ASKED_FOR_FEEDBACK = True
Edward Lemur6eb1d322020-02-27 22:20:15 +00001385
1386 return 1 if presubmits_failed else 0
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001387 finally:
1388 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001389
1390
Edward Lemur50984a62020-02-06 18:10:18 +00001391def _scan_sub_dirs(mask, recursive):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001392 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001393 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001394
1395 results = []
1396 for root, dirs, files in os.walk('.'):
1397 if '.svn' in dirs:
1398 dirs.remove('.svn')
1399 if '.git' in dirs:
1400 dirs.remove('.git')
1401 for name in files:
1402 if fnmatch.fnmatch(name, mask):
1403 results.append(os.path.join(root, name))
1404 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001405
1406
Edward Lemur50984a62020-02-06 18:10:18 +00001407def _parse_files(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001408 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001409 files = []
1410 for arg in args:
Edward Lemur50984a62020-02-06 18:10:18 +00001411 files.extend([('M', f) for f in _scan_sub_dirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001412 return files
1413
1414
Edward Lemur50984a62020-02-06 18:10:18 +00001415def _parse_change(parser, options):
1416 """Process change options.
1417
1418 Args:
1419 parser: The parser used to parse the arguments from command line.
1420 options: The arguments parsed from command line.
1421 Returns:
Josip Sokcevic688adfe2023-03-01 22:46:12 +00001422 A change.GitChange if the change root is a git repository, or a Change
1423 otherwise.
Edward Lemur50984a62020-02-06 18:10:18 +00001424 """
1425 if options.files and options.all_files:
1426 parser.error('<files> cannot be specified when --all-files is set.')
1427
Edward Lesmes44ea3ff2020-02-05 00:14:30 +00001428 change_scm = scm.determine_scm(options.root)
Edward Lemur50984a62020-02-06 18:10:18 +00001429 if change_scm != 'git' and not options.files:
1430 parser.error('<files> is not optional for unversioned directories.')
1431
1432 if options.files:
Josip Sokcevic017544d2022-03-31 23:47:53 +00001433 if options.source_controlled_only:
1434 # Get the filtered set of files from SCM.
1435 change_files = []
1436 for name in scm.GIT.GetAllFiles(options.root):
1437 for mask in options.files:
1438 if fnmatch.fnmatch(name, mask):
1439 change_files.append(('M', name))
1440 break
1441 else:
1442 # Get the filtered set of files from a directory scan.
1443 change_files = _parse_files(options.files, options.recursive)
Edward Lemur50984a62020-02-06 18:10:18 +00001444 elif options.all_files:
1445 change_files = [('M', f) for f in scm.GIT.GetAllFiles(options.root)]
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001446 else:
Edward Lemur50984a62020-02-06 18:10:18 +00001447 change_files = scm.GIT.CaptureStatus(
Edward Lemur7f6dec02020-02-06 20:23:58 +00001448 options.root, options.upstream or None)
Edward Lemur50984a62020-02-06 18:10:18 +00001449
1450 logging.info('Found %d file(s).', len(change_files))
1451
Josip Sokcevic688adfe2023-03-01 22:46:12 +00001452 change_class = libchange.GitChange if change_scm == 'git' \
1453 else libchange.Change
Edward Lemur50984a62020-02-06 18:10:18 +00001454 return change_class(
1455 options.name,
1456 options.description,
1457 options.root,
1458 change_files,
1459 options.issue,
1460 options.patchset,
1461 options.author,
1462 upstream=options.upstream)
1463
1464
1465def _parse_gerrit_options(parser, options):
1466 """Process gerrit options.
1467
1468 SIDE EFFECTS: Modifies options.author and options.description from Gerrit if
1469 options.gerrit_fetch is set.
1470
1471 Args:
1472 parser: The parser used to parse the arguments from command line.
1473 options: The arguments parsed from command line.
1474 Returns:
Stephen Martinisfb09de22021-02-25 03:41:13 +00001475 A GerritAccessor object if options.gerrit_url is set, or None otherwise.
Edward Lemur50984a62020-02-06 18:10:18 +00001476 """
1477 gerrit_obj = None
Stephen Martinisfb09de22021-02-25 03:41:13 +00001478 if options.gerrit_url:
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001479 gerrit_obj = GerritAccessor(
1480 url=options.gerrit_url,
1481 project=options.gerrit_project,
1482 branch=options.gerrit_branch)
Edward Lemur50984a62020-02-06 18:10:18 +00001483
1484 if not options.gerrit_fetch:
1485 return gerrit_obj
1486
1487 if not options.gerrit_url or not options.issue or not options.patchset:
1488 parser.error(
1489 '--gerrit_fetch requires --gerrit_url, --issue and --patchset.')
1490
1491 options.author = gerrit_obj.GetChangeOwner(options.issue)
1492 options.description = gerrit_obj.GetChangeDescription(
1493 options.issue, options.patchset)
1494
1495 logging.info('Got author: "%s"', options.author)
1496 logging.info('Got description: """\n%s\n"""', options.description)
1497
1498 return gerrit_obj
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001499
1500
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001501@contextlib.contextmanager
1502def canned_check_filter(method_names):
1503 filtered = {}
1504 try:
1505 for method_name in method_names:
1506 if not hasattr(presubmit_canned_checks, method_name):
Gavin Make6a62332020-12-04 21:57:10 +00001507 logging.warning('Skipping unknown "canned" check %s' % method_name)
Aaron Gableecee74c2018-04-02 15:13:08 -07001508 continue
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001509 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1510 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1511 yield
1512 finally:
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001513 for name, method in filtered.items():
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001514 setattr(presubmit_canned_checks, name, method)
1515
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001516
sbc@chromium.org013731e2015-02-26 18:28:43 +00001517def main(argv=None):
Edward Lemura5799e32020-01-17 19:26:51 +00001518 parser = argparse.ArgumentParser(usage='%(prog)s [options] <files...>')
1519 hooks = parser.add_mutually_exclusive_group()
1520 hooks.add_argument('-c', '--commit', action='store_true',
1521 help='Use commit instead of upload checks.')
1522 hooks.add_argument('-u', '--upload', action='store_false', dest='commit',
1523 help='Use upload instead of commit checks.')
Edward Lemur75526302020-02-27 22:31:05 +00001524 hooks.add_argument('--post_upload', action='store_true',
1525 help='Run post-upload commit hooks.')
Edward Lemura5799e32020-01-17 19:26:51 +00001526 parser.add_argument('-r', '--recursive', action='store_true',
1527 help='Act recursively.')
1528 parser.add_argument('-v', '--verbose', action='count', default=0,
1529 help='Use 2 times for more debug info.')
1530 parser.add_argument('--name', default='no name')
1531 parser.add_argument('--author')
Edward Lemur227d5102020-02-25 23:45:35 +00001532 desc = parser.add_mutually_exclusive_group()
1533 desc.add_argument('--description', default='', help='The change description.')
1534 desc.add_argument('--description_file',
1535 help='File to read change description from.')
Edward Lemura5799e32020-01-17 19:26:51 +00001536 parser.add_argument('--issue', type=int, default=0)
1537 parser.add_argument('--patchset', type=int, default=0)
1538 parser.add_argument('--root', default=os.getcwd(),
1539 help='Search for PRESUBMIT.py up to this directory. '
1540 'If inherit-review-settings-ok is present in this '
1541 'directory, parent directories up to the root file '
1542 'system directories will also be searched.')
1543 parser.add_argument('--upstream',
1544 help='Git only: the base ref or upstream branch against '
1545 'which the diff should be computed.')
1546 parser.add_argument('--default_presubmit')
1547 parser.add_argument('--may_prompt', action='store_true', default=False)
1548 parser.add_argument('--skip_canned', action='append', default=[],
1549 help='A list of checks to skip which appear in '
1550 'presubmit_canned_checks. Can be provided multiple times '
1551 'to skip multiple canned checks.')
1552 parser.add_argument('--dry_run', action='store_true', help=argparse.SUPPRESS)
1553 parser.add_argument('--gerrit_url', help=argparse.SUPPRESS)
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001554 parser.add_argument('--gerrit_project', help=argparse.SUPPRESS)
1555 parser.add_argument('--gerrit_branch', help=argparse.SUPPRESS)
Edward Lemura5799e32020-01-17 19:26:51 +00001556 parser.add_argument('--gerrit_fetch', action='store_true',
1557 help=argparse.SUPPRESS)
1558 parser.add_argument('--parallel', action='store_true',
1559 help='Run all tests specified by input_api.RunTests in '
1560 'all PRESUBMIT files in parallel.')
1561 parser.add_argument('--json_output',
1562 help='Write presubmit errors to json output.')
Edward Lemur1dc66e12020-02-21 21:36:34 +00001563 parser.add_argument('--all_files', action='store_true',
Edward Lemur50984a62020-02-06 18:10:18 +00001564 help='Mark all files under source control as modified.')
Bruce Dawson09c0c072022-05-26 20:28:58 +00001565
Edward Lemura5799e32020-01-17 19:26:51 +00001566 parser.add_argument('files', nargs='*',
1567 help='List of files to be marked as modified when '
1568 'executing presubmit or post-upload hooks. fnmatch '
1569 'wildcards can also be used.')
Josip Sokcevic017544d2022-03-31 23:47:53 +00001570 parser.add_argument('--source_controlled_only', action='store_true',
1571 help='Constrain \'files\' to those in source control.')
Dirk Pranke6f0df682021-06-25 00:42:33 +00001572 parser.add_argument('--use-python3', action='store_true',
1573 help='Use python3 for presubmit checks by default')
Bruce Dawson09c0c072022-05-26 20:28:58 +00001574 parser.add_argument('--no_diffs', action='store_true',
1575 help='Assume that all "modified" files have no diffs.')
Edward Lemura5799e32020-01-17 19:26:51 +00001576 options = parser.parse_args(argv)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001577
Erik Staabcca5c492020-04-16 17:40:07 +00001578 log_level = logging.ERROR
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001579 if options.verbose >= 2:
Erik Staabcca5c492020-04-16 17:40:07 +00001580 log_level = logging.DEBUG
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001581 elif options.verbose:
Erik Staabcca5c492020-04-16 17:40:07 +00001582 log_level = logging.INFO
1583 log_format = ('[%(levelname).1s%(asctime)s %(process)d %(thread)d '
1584 '%(filename)s] %(message)s')
1585 logging.basicConfig(format=log_format, level=log_level)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001586
Bruce Dawsondca14bc2022-09-15 20:59:38 +00001587 # Print call stacks when _PresubmitResult objects are created with -v -v is
1588 # specified. This helps track down where presubmit messages are coming from.
1589 if options.verbose >= 2:
1590 global _SHOW_CALLSTACKS
1591 _SHOW_CALLSTACKS = True
1592
Edward Lemur227d5102020-02-25 23:45:35 +00001593 if options.description_file:
1594 options.description = gclient_utils.FileRead(options.description_file)
Edward Lemur50984a62020-02-06 18:10:18 +00001595 gerrit_obj = _parse_gerrit_options(parser, options)
1596 change = _parse_change(parser, options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001597
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001598 try:
Edward Lemur75526302020-02-27 22:31:05 +00001599 if options.post_upload:
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001600 return DoPostUploadExecuter(change, gerrit_obj, options.verbose,
1601 options.use_python3)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001602 with canned_check_filter(options.skip_canned):
Edward Lemur6eb1d322020-02-27 22:20:15 +00001603 return DoPresubmitChecks(
Edward Lemur50984a62020-02-06 18:10:18 +00001604 change,
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001605 options.commit,
1606 options.verbose,
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001607 options.default_presubmit,
1608 options.may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001609 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04001610 options.dry_run,
Debrian Figueroadd2737e2019-06-21 23:50:13 +00001611 options.parallel,
Dirk Pranke6f0df682021-06-25 00:42:33 +00001612 options.json_output,
Bruce Dawson09c0c072022-05-26 20:28:58 +00001613 options.use_python3,
1614 options.no_diffs)
Raul Tambre7c938462019-05-24 16:35:35 +00001615 except PresubmitFailure as e:
Raul Tambre80ee78e2019-05-06 22:41:05 +00001616 print(e, file=sys.stderr)
1617 print('Maybe your depot_tools is out of date?', file=sys.stderr)
Josip Sokcevic0399e172022-03-21 23:11:51 +00001618 print('depot_tools version: %s' % utils.depot_tools_version(),
1619 file=sys.stderr)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001620 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001621
1622
1623if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001624 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001625 try:
1626 sys.exit(main())
1627 except KeyboardInterrupt:
1628 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001629 sys.exit(2)