blob: 3d6a9bd9d724f7c236cb0a02b2cdaff829520b45 [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
Aaron Gableb584c4f2017-04-26 16:28:08 -070045import git_footers
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000046import gerrit_util
Edward Lesmes9ce03f82021-01-12 20:13:31 +000047import owners as owners_db
48import owners_client
Jochen Eisinger76f5fc62017-04-07 16:27:46 +020049import owners_finder
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050import presubmit_canned_checks
Saagar Sanghavi9949ab72020-07-20 20:56:40 +000051import rdb_wrapper
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000052import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000053import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000054
Edward Lemur16af3562019-10-17 22:11:33 +000055if sys.version_info.major == 2:
56 # TODO(1009814): Expose urllib2 only through urllib_request and urllib_error
57 import urllib2 # Exposed through the API.
58 import urlparse
59 import urllib2 as urllib_request
60 import urllib2 as urllib_error
61else:
62 import urllib.parse as urlparse
63 import urllib.request as urllib_request
64 import urllib.error as urllib_error
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000065
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000066# Ask for feedback only once in program lifetime.
67_ASKED_FOR_FEEDBACK = False
68
Edward Lemurecc27072020-01-06 16:42:34 +000069def time_time():
70 # Use this so that it can be mocked in tests without interfering with python
71 # system machinery.
72 return time.time()
73
74
maruel@chromium.org899e1c12011-04-07 17:03:18 +000075class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000076 pass
77
78
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000079class CommandData(object):
Edward Lemur940c2822019-08-23 00:34:25 +000080 def __init__(self, name, cmd, kwargs, message, python3=False):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000081 self.name = name
82 self.cmd = cmd
Edward Lesmes8e282792018-04-03 18:50:29 -040083 self.stdin = kwargs.get('stdin', None)
Edward Lemur2d6b67c2019-08-23 22:25:41 +000084 self.kwargs = kwargs.copy()
Edward Lesmes8e282792018-04-03 18:50:29 -040085 self.kwargs['stdout'] = subprocess.PIPE
86 self.kwargs['stderr'] = subprocess.STDOUT
87 self.kwargs['stdin'] = subprocess.PIPE
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000088 self.message = message
89 self.info = None
Edward Lemur940c2822019-08-23 00:34:25 +000090 self.python3 = python3
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000091
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000092
Edward Lesmes8e282792018-04-03 18:50:29 -040093# Adapted from
94# https://github.com/google/gtest-parallel/blob/master/gtest_parallel.py#L37
95#
96# An object that catches SIGINT sent to the Python process and notices
97# if processes passed to wait() die by SIGINT (we need to look for
98# both of those cases, because pressing Ctrl+C can result in either
99# the main process or one of the subprocesses getting the signal).
100#
101# Before a SIGINT is seen, wait(p) will simply call p.wait() and
102# return the result. Once a SIGINT has been seen (in the main process
103# or a subprocess, including the one the current call is waiting for),
Edward Lemur9a5bb612019-09-26 02:01:52 +0000104# wait(p) will call p.terminate().
Edward Lesmes8e282792018-04-03 18:50:29 -0400105class SigintHandler(object):
Edward Lesmes8e282792018-04-03 18:50:29 -0400106 sigint_returncodes = {-signal.SIGINT, # Unix
107 -1073741510, # Windows
108 }
109 def __init__(self):
110 self.__lock = threading.Lock()
111 self.__processes = set()
112 self.__got_sigint = False
Edward Lemur9a5bb612019-09-26 02:01:52 +0000113 self.__previous_signal = signal.signal(signal.SIGINT, self.interrupt)
Edward Lesmes8e282792018-04-03 18:50:29 -0400114
115 def __on_sigint(self):
116 self.__got_sigint = True
117 while self.__processes:
118 try:
119 self.__processes.pop().terminate()
120 except OSError:
121 pass
122
Edward Lemur9a5bb612019-09-26 02:01:52 +0000123 def interrupt(self, signal_num, frame):
Edward Lesmes8e282792018-04-03 18:50:29 -0400124 with self.__lock:
125 self.__on_sigint()
Edward Lemur9a5bb612019-09-26 02:01:52 +0000126 self.__previous_signal(signal_num, frame)
Edward Lesmes8e282792018-04-03 18:50:29 -0400127
128 def got_sigint(self):
129 with self.__lock:
130 return self.__got_sigint
131
132 def wait(self, p, stdin):
133 with self.__lock:
134 if self.__got_sigint:
135 p.terminate()
136 self.__processes.add(p)
137 stdout, stderr = p.communicate(stdin)
138 code = p.returncode
139 with self.__lock:
140 self.__processes.discard(p)
141 if code in self.sigint_returncodes:
142 self.__on_sigint()
Edward Lesmes8e282792018-04-03 18:50:29 -0400143 return stdout, stderr
144
145sigint_handler = SigintHandler()
146
147
Edward Lemurecc27072020-01-06 16:42:34 +0000148class Timer(object):
149 def __init__(self, timeout, fn):
150 self.completed = False
151 self._fn = fn
152 self._timer = threading.Timer(timeout, self._onTimer) if timeout else None
153
154 def __enter__(self):
155 if self._timer:
156 self._timer.start()
157 return self
158
159 def __exit__(self, _type, _value, _traceback):
160 if self._timer:
161 self._timer.cancel()
162
163 def _onTimer(self):
164 self._fn()
165 self.completed = True
166
167
Edward Lesmes8e282792018-04-03 18:50:29 -0400168class ThreadPool(object):
Edward Lemurecc27072020-01-06 16:42:34 +0000169 def __init__(self, pool_size=None, timeout=None):
170 self.timeout = timeout
Edward Lesmes8e282792018-04-03 18:50:29 -0400171 self._pool_size = pool_size or multiprocessing.cpu_count()
Bruce Dawson8254f062022-06-17 17:33:08 +0000172 if sys.platform == 'win32':
173 # TODO(crbug.com/1190269) - we can't use more than 56 child processes on
174 # Windows or Python3 may hang.
175 self._pool_size = min(self._pool_size, 56)
Edward Lesmes8e282792018-04-03 18:50:29 -0400176 self._messages = []
177 self._messages_lock = threading.Lock()
178 self._tests = []
179 self._tests_lock = threading.Lock()
180 self._nonparallel_tests = []
181
Edward Lemurecc27072020-01-06 16:42:34 +0000182 def _GetCommand(self, test):
Edward Lemur940c2822019-08-23 00:34:25 +0000183 vpython = 'vpython'
184 if test.python3:
185 vpython += '3'
186 if sys.platform == 'win32':
187 vpython += '.bat'
Edward Lesmes8e282792018-04-03 18:50:29 -0400188
189 cmd = test.cmd
190 if cmd[0] == 'python':
191 cmd = list(cmd)
192 cmd[0] = vpython
193 elif cmd[0].endswith('.py'):
194 cmd = [vpython] + cmd
195
Edward Lemur336e51f2019-11-14 21:42:04 +0000196 # On Windows, scripts on the current directory take precedence over PATH, so
197 # that when testing depot_tools on Windows, calling `vpython.bat` will
198 # execute the copy of vpython of the depot_tools under test instead of the
199 # one in the bot.
200 # As a workaround, we run the tests from the parent directory instead.
201 if (cmd[0] == vpython and
202 'cwd' in test.kwargs and
203 os.path.basename(test.kwargs['cwd']) == 'depot_tools'):
204 test.kwargs['cwd'] = os.path.dirname(test.kwargs['cwd'])
205 cmd[1] = os.path.join('depot_tools', cmd[1])
206
Edward Lemurecc27072020-01-06 16:42:34 +0000207 return cmd
208
209 def _RunWithTimeout(self, cmd, stdin, kwargs):
210 p = subprocess.Popen(cmd, **kwargs)
211 with Timer(self.timeout, p.terminate) as timer:
212 stdout, _ = sigint_handler.wait(p, stdin)
Josip Sokcevic6635baf2021-11-19 02:04:39 +0000213 stdout = stdout.decode('utf-8', 'ignore')
Edward Lemurecc27072020-01-06 16:42:34 +0000214 if timer.completed:
215 stdout = 'Process timed out after %ss\n%s' % (self.timeout, stdout)
Josip Sokcevic6635baf2021-11-19 02:04:39 +0000216 return p.returncode, stdout
Edward Lemurecc27072020-01-06 16:42:34 +0000217
218 def CallCommand(self, test):
219 """Runs an external program.
220
Edward Lemura5799e32020-01-17 19:26:51 +0000221 This function converts invocation of .py files and invocations of 'python'
Edward Lemurecc27072020-01-06 16:42:34 +0000222 to vpython invocations.
223 """
224 cmd = self._GetCommand(test)
Edward Lesmes8e282792018-04-03 18:50:29 -0400225 try:
Edward Lemurecc27072020-01-06 16:42:34 +0000226 start = time_time()
227 returncode, stdout = self._RunWithTimeout(cmd, test.stdin, test.kwargs)
228 duration = time_time() - start
Edward Lemur7cf94382019-11-15 22:36:41 +0000229 except Exception:
Edward Lemurecc27072020-01-06 16:42:34 +0000230 duration = time_time() - start
Edward Lesmes8e282792018-04-03 18:50:29 -0400231 return test.message(
Edward Lemur7cf94382019-11-15 22:36:41 +0000232 '%s\n%s exec failure (%4.2fs)\n%s' % (
233 test.name, ' '.join(cmd), duration, traceback.format_exc()))
Edward Lemurde9e3ca2019-10-24 21:13:31 +0000234
Edward Lemurecc27072020-01-06 16:42:34 +0000235 if returncode != 0:
Edward Lesmes8e282792018-04-03 18:50:29 -0400236 return test.message(
Edward Lemur7cf94382019-11-15 22:36:41 +0000237 '%s\n%s (%4.2fs) failed\n%s' % (
238 test.name, ' '.join(cmd), duration, stdout))
Edward Lemurecc27072020-01-06 16:42:34 +0000239
Edward Lesmes8e282792018-04-03 18:50:29 -0400240 if test.info:
Edward Lemur7cf94382019-11-15 22:36:41 +0000241 return test.info('%s\n%s (%4.2fs)' % (test.name, ' '.join(cmd), duration))
Edward Lesmes8e282792018-04-03 18:50:29 -0400242
243 def AddTests(self, tests, parallel=True):
244 if parallel:
245 self._tests.extend(tests)
246 else:
247 self._nonparallel_tests.extend(tests)
248
249 def RunAsync(self):
250 self._messages = []
251
252 def _WorkerFn():
253 while True:
254 test = None
255 with self._tests_lock:
256 if not self._tests:
257 break
258 test = self._tests.pop()
259 result = self.CallCommand(test)
260 if result:
261 with self._messages_lock:
262 self._messages.append(result)
263
264 def _StartDaemon():
265 t = threading.Thread(target=_WorkerFn)
266 t.daemon = True
267 t.start()
268 return t
269
270 while self._nonparallel_tests:
271 test = self._nonparallel_tests.pop()
272 result = self.CallCommand(test)
273 if result:
274 self._messages.append(result)
275
276 if self._tests:
277 threads = [_StartDaemon() for _ in range(self._pool_size)]
278 for worker in threads:
279 worker.join()
280
281 return self._messages
282
283
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000284def normpath(path):
285 '''Version of os.path.normpath that also changes backward slashes to
286 forward slashes when not running on Windows.
287 '''
288 # This is safe to always do because the Windows version of os.path.normpath
289 # will replace forward slashes with backward slashes.
290 path = path.replace(os.sep, '/')
291 return os.path.normpath(path)
292
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000293
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000294def _RightHandSideLinesImpl(affected_files):
295 """Implements RightHandSideLines for InputApi and GclChange."""
296 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000297 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000298 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000299 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000300
301
Edward Lemur6eb1d322020-02-27 22:20:15 +0000302def prompt_should_continue(prompt_string):
303 sys.stdout.write(prompt_string)
Dirk Pranke7288f882021-06-03 18:01:30 +0000304 sys.stdout.flush()
Edward Lemur6eb1d322020-02-27 22:20:15 +0000305 response = sys.stdin.readline().strip().lower()
306 return response in ('y', 'yes')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000307
308
Josip Sokcevice293d3d2022-02-16 22:52:15 +0000309def _ShouldRunPresubmit(script_text, use_python3):
310 """Try to figure out whether these presubmit checks should be run under
311 python2 or python3. We need to do this without actually trying to
312 compile the text, since the text might compile in one but not the
313 other.
314
315 Args:
316 script_text: The text of the presubmit script.
317 use_python3: if true, will use python3 instead of python2 by default
318 if USE_PYTHON3 is not specified.
319
320 Return:
321 A boolean if presubmit should be executed
322 """
323 m = re.search('^USE_PYTHON3 = (True|False)$', script_text, flags=re.MULTILINE)
324 if m:
325 use_python3 = m.group(1) == 'True'
326
327 return ((sys.version_info.major == 2) and not use_python3) or \
328 ((sys.version_info.major == 3) and use_python3)
329
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000330# Top level object so multiprocessing can pickle
331# Public access through OutputApi object.
332class _PresubmitResult(object):
333 """Base class for result objects."""
334 fatal = False
335 should_prompt = False
336
337 def __init__(self, message, items=None, long_text=''):
338 """
339 message: A short one-line message to indicate errors.
340 items: A list of short strings to indicate where errors occurred.
341 long_text: multi-line text output, e.g. from another tool
342 """
Bruce Dawsondb8622b2022-04-03 15:38:12 +0000343 self._message = _PresubmitResult._ensure_str(message)
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000344 self._items = items or []
Tom McKee61c72652021-07-20 11:56:32 +0000345 self._long_text = _PresubmitResult._ensure_str(long_text.rstrip())
346
347 @staticmethod
348 def _ensure_str(val):
349 """
350 val: A "stringish" value. Can be any of str, unicode or bytes.
351 returns: A str after applying encoding/decoding as needed.
352 Assumes/uses UTF-8 for relevant inputs/outputs.
353
354 We'd prefer to use six.ensure_str but our copy of six is old :(
355 """
356 if isinstance(val, str):
357 return val
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000358
Tom McKee61c72652021-07-20 11:56:32 +0000359 if six.PY2 and isinstance(val, unicode):
360 return val.encode()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000361
362 if six.PY3 and isinstance(val, bytes):
Tom McKee61c72652021-07-20 11:56:32 +0000363 return val.decode()
364 raise ValueError("Unknown string type %s" % type(val))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000365
Edward Lemur6eb1d322020-02-27 22:20:15 +0000366 def handle(self):
367 sys.stdout.write(self._message)
368 sys.stdout.write('\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000369 for index, item in enumerate(self._items):
Edward Lemur6eb1d322020-02-27 22:20:15 +0000370 sys.stdout.write(' ')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000371 # Write separately in case it's unicode.
Edward Lemur6eb1d322020-02-27 22:20:15 +0000372 sys.stdout.write(str(item))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000373 if index < len(self._items) - 1:
Edward Lemur6eb1d322020-02-27 22:20:15 +0000374 sys.stdout.write(' \\')
375 sys.stdout.write('\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000376 if 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 # Write separately in case it's unicode.
Tom McKee61c72652021-07-20 11:56:32 +0000379 sys.stdout.write(self._long_text)
Edward Lemur6eb1d322020-02-27 22:20:15 +0000380 sys.stdout.write('\n***************\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000381
Debrian Figueroadd2737e2019-06-21 23:50:13 +0000382 def json_format(self):
383 return {
384 'message': self._message,
Debrian Figueroa6095d402019-06-28 18:47:18 +0000385 'items': [str(item) for item in self._items],
Debrian Figueroadd2737e2019-06-21 23:50:13 +0000386 'long_text': self._long_text,
387 'fatal': self.fatal
388 }
389
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000390
391# Top level object so multiprocessing can pickle
392# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000393class _PresubmitError(_PresubmitResult):
394 """A hard presubmit error."""
395 fatal = True
396
397
398# Top level object so multiprocessing can pickle
399# Public access through OutputApi object.
400class _PresubmitPromptWarning(_PresubmitResult):
401 """An warning that prompts the user if they want to continue."""
402 should_prompt = True
403
404
405# Top level object so multiprocessing can pickle
406# Public access through OutputApi object.
407class _PresubmitNotifyResult(_PresubmitResult):
408 """Just print something to the screen -- but it's not even a warning."""
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000409
410
411# Top level object so multiprocessing can pickle
412# Public access through OutputApi object.
413class _MailTextResult(_PresubmitResult):
414 """A warning that should be included in the review request email."""
415 def __init__(self, *args, **kwargs):
416 super(_MailTextResult, self).__init__()
417 raise NotImplementedError()
418
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000419class GerritAccessor(object):
420 """Limited Gerrit functionality for canned presubmit checks to work.
421
422 To avoid excessive Gerrit calls, caches the results.
423 """
424
Edward Lesmeseb1bd622021-03-01 19:54:07 +0000425 def __init__(self, url=None, project=None, branch=None):
426 self.host = urlparse.urlparse(url).netloc if url else None
427 self.project = project
428 self.branch = branch
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000429 self.cache = {}
Edward Lesmes8170c292021-03-19 20:04:43 +0000430 self.code_owners_enabled = None
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000431
432 def _FetchChangeDetail(self, issue):
433 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100434 try:
435 return gerrit_util.GetChangeDetail(
436 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700437 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100438 except gerrit_util.GerritError as e:
439 if e.http_status == 404:
440 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
441 'no credentials to fetch issue details' % issue)
442 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000443
444 def GetChangeInfo(self, issue):
445 """Returns labels and all revisions (patchsets) for this issue.
446
447 The result is a dictionary according to Gerrit REST Api.
448 https://gerrit-review.googlesource.com/Documentation/rest-api.html
449
450 However, API isn't very clear what's inside, so see tests for example.
451 """
452 assert issue
453 cache_key = int(issue)
454 if cache_key not in self.cache:
455 self.cache[cache_key] = self._FetchChangeDetail(issue)
456 return self.cache[cache_key]
457
458 def GetChangeDescription(self, issue, patchset=None):
459 """If patchset is none, fetches current patchset."""
460 info = self.GetChangeInfo(issue)
461 # info is a reference to cache. We'll modify it here adding description to
462 # it to the right patchset, if it is not yet there.
463
464 # Find revision info for the patchset we want.
465 if patchset is not None:
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000466 for rev, rev_info in info['revisions'].items():
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000467 if str(rev_info['_number']) == str(patchset):
468 break
469 else:
470 raise Exception('patchset %s doesn\'t exist in issue %s' % (
471 patchset, issue))
472 else:
473 rev = info['current_revision']
474 rev_info = info['revisions'][rev]
475
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100476 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000477
Mun Yong Jang603d01e2017-12-19 16:38:30 -0800478 def GetDestRef(self, issue):
479 ref = self.GetChangeInfo(issue)['branch']
480 if not ref.startswith('refs/'):
481 # NOTE: it is possible to create 'refs/x' branch,
482 # aka 'refs/heads/refs/x'. However, this is ill-advised.
483 ref = 'refs/heads/%s' % ref
484 return ref
485
Edward Lesmes02d4b822020-11-11 00:37:35 +0000486 def _GetApproversForLabel(self, issue, label):
487 change_info = self.GetChangeInfo(issue)
488 label_info = change_info.get('labels', {}).get(label, {})
489 values = label_info.get('values', {}).keys()
490 if not values:
491 return []
492 max_value = max(int(v) for v in values)
493 return [v for v in label_info.get('all', [])
494 if v.get('value', 0) == max_value]
495
Edward Lesmesc4566172021-03-19 16:55:13 +0000496 def IsBotCommitApproved(self, issue):
497 return bool(self._GetApproversForLabel(issue, 'Bot-Commit'))
498
Edward Lesmescf49cb82020-11-11 01:08:36 +0000499 def IsOwnersOverrideApproved(self, issue):
500 return bool(self._GetApproversForLabel(issue, 'Owners-Override'))
501
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000502 def GetChangeOwner(self, issue):
503 return self.GetChangeInfo(issue)['owner']['email']
504
505 def GetChangeReviewers(self, issue, approving_only=True):
Aaron Gable8b478f02017-07-31 15:33:19 -0700506 changeinfo = self.GetChangeInfo(issue)
507 if approving_only:
Edward Lesmes02d4b822020-11-11 00:37:35 +0000508 reviewers = self._GetApproversForLabel(issue, 'Code-Review')
Aaron Gable8b478f02017-07-31 15:33:19 -0700509 else:
510 reviewers = changeinfo.get('reviewers', {}).get('REVIEWER', [])
511 return [r.get('email') for r in reviewers]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000512
Edward Lemure4d329c2020-02-03 20:41:18 +0000513 def UpdateDescription(self, description, issue):
514 gerrit_util.SetCommitMessage(self.host, issue, description, notify='NONE')
515
Edward Lesmes8170c292021-03-19 20:04:43 +0000516 def IsCodeOwnersEnabledOnRepo(self):
517 if self.code_owners_enabled is None:
518 self.code_owners_enabled = gerrit_util.IsCodeOwnersEnabledOnRepo(
Edward Lesmes392c4072021-03-19 21:58:45 +0000519 self.host, self.project)
Edward Lesmes8170c292021-03-19 20:04:43 +0000520 return self.code_owners_enabled
521
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000522
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000523class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000524 """An instance of OutputApi gets passed to presubmit scripts so that they
525 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000526 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000527 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000528 PresubmitError = _PresubmitError
529 PresubmitPromptWarning = _PresubmitPromptWarning
530 PresubmitNotifyResult = _PresubmitNotifyResult
531 MailTextResult = _MailTextResult
532
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000533 def __init__(self, is_committing):
534 self.is_committing = is_committing
Daniel Cheng7227d212017-11-17 08:12:37 -0800535 self.more_cc = []
536
537 def AppendCC(self, cc):
538 """Appends a user to cc for this change."""
539 self.more_cc.append(cc)
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000540
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000541 def PresubmitPromptOrNotify(self, *args, **kwargs):
542 """Warn the user when uploading, but only notify if committing."""
543 if self.is_committing:
544 return self.PresubmitNotifyResult(*args, **kwargs)
545 return self.PresubmitPromptWarning(*args, **kwargs)
546
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000547
548class InputApi(object):
549 """An instance of this object is passed to presubmit scripts so they can
550 know stuff about the change they're looking at.
551 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000552 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800553 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000554
maruel@chromium.org3410d912009-06-09 20:56:16 +0000555 # File extensions that are considered source files from a style guide
556 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000557 #
558 # Files without an extension aren't included in the list. If you want to
local_bot30f774e2020-06-25 18:23:34 +0000559 # filter them as source files, add r'(^|.*?[\\\/])[^.]+$' to the allow list.
local_bot64021412020-07-08 21:05:39 +0000560 # Note that ALL CAPS files are skipped in DEFAULT_FILES_TO_SKIP below.
561 DEFAULT_FILES_TO_CHECK = (
maruel@chromium.org3410d912009-06-09 20:56:16 +0000562 # C++ and friends
Edward Lemura5799e32020-01-17 19:26:51 +0000563 r'.+\.c$', r'.+\.cc$', r'.+\.cpp$', r'.+\.h$', r'.+\.m$', r'.+\.mm$',
564 r'.+\.inl$', r'.+\.asm$', r'.+\.hxx$', r'.+\.hpp$', r'.+\.s$', r'.+\.S$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000565 # Scripts
dpapad443d9132022-05-05 00:17:30 +0000566 r'.+\.js$', r'.+\.ts$', r'.+\.py$', r'.+\.sh$', r'.+\.rb$', r'.+\.pl$',
567 r'.+\.pm$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000568 # Other
Edward Lemura5799e32020-01-17 19:26:51 +0000569 r'.+\.java$', r'.+\.mk$', r'.+\.am$', r'.+\.css$', r'.+\.mojom$',
570 r'.+\.fidl$'
maruel@chromium.org3410d912009-06-09 20:56:16 +0000571 )
572
573 # Path regexp that should be excluded from being considered containing source
574 # files. Don't modify this list from a presubmit script!
local_bot64021412020-07-08 21:05:39 +0000575 DEFAULT_FILES_TO_SKIP = (
Edward Lemura5799e32020-01-17 19:26:51 +0000576 r'testing_support[\\\/]google_appengine[\\\/].*',
577 r'.*\bexperimental[\\\/].*',
Kent Tamura179dd1e2018-04-26 15:07:41 +0900578 # Exclude third_party/.* but NOT third_party/{WebKit,blink}
579 # (crbug.com/539768 and crbug.com/836555).
Edward Lemura5799e32020-01-17 19:26:51 +0000580 r'.*\bthird_party[\\\/](?!(WebKit|blink)[\\\/]).*',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000581 # Output directories (just in case)
Edward Lemura5799e32020-01-17 19:26:51 +0000582 r'.*\bDebug[\\\/].*',
583 r'.*\bRelease[\\\/].*',
584 r'.*\bxcodebuild[\\\/].*',
585 r'.*\bout[\\\/].*',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000586 # All caps files like README and LICENCE.
Edward Lemura5799e32020-01-17 19:26:51 +0000587 r'.*\b[A-Z0-9_]{2,}$',
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000588 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
Edward Lemura5799e32020-01-17 19:26:51 +0000589 r'(|.*[\\\/])\.git[\\\/].*',
590 r'(|.*[\\\/])\.svn[\\\/].*',
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000591 # There is no point in processing a patch file.
Edward Lemura5799e32020-01-17 19:26:51 +0000592 r'.+\.diff$',
593 r'.+\.patch$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000594 )
595
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000596 def __init__(self, change, presubmit_path, is_committing,
Bruce Dawson09c0c072022-05-26 20:28:58 +0000597 verbose, gerrit_obj, dry_run=None, thread_pool=None, parallel=False,
598 no_diffs=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000599 """Builds an InputApi object.
600
601 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000602 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000603 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000604 is_committing: True if the change is about to be committed.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000605 gerrit_obj: provides basic Gerrit codereview functionality.
606 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -0400607 parallel: if true, all tests reported via input_api.RunTests for all
608 PRESUBMIT files will be run in parallel.
Bruce Dawson09c0c072022-05-26 20:28:58 +0000609 no_diffs: if true, implies that --files or --all was specified so some
610 checks can be skipped, and some errors will be messages.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000611 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000612 # Version number of the presubmit_support script.
613 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000614 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000615 self.is_committing = is_committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000616 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000617 self.dry_run = dry_run
Bruce Dawson09c0c072022-05-26 20:28:58 +0000618 self.no_diffs = no_diffs
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000619
Edward Lesmes8e282792018-04-03 18:50:29 -0400620 self.parallel = parallel
621 self.thread_pool = thread_pool or ThreadPool()
622
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000623 # We expose various modules and functions as attributes of the input_api
624 # so that presubmit scripts don't have to import them.
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +0900625 self.ast = ast
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000626 self.basename = os.path.basename
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000627 self.cpplint = cpplint
dcheng091b7db2016-06-16 01:27:51 -0700628 self.fnmatch = fnmatch
Yoshisato Yanagisawa04600b42019-03-15 03:03:41 +0000629 self.gclient_paths = gclient_paths
Yoshisato Yanagisawa57dd17b2019-03-22 09:10:29 +0000630 # TODO(yyanagisawa): stop exposing this when python3 become default.
631 # Since python3's tempfile has TemporaryDirectory, we do not need this.
632 self.temporary_directory = gclient_utils.temporary_directory
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000633 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000634 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000635 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000636 self.os_listdir = os.listdir
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000637 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000638 self.os_stat = os.stat
Yoshisato Yanagisawa406de132018-06-29 05:43:25 +0000639 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000640 self.re = re
641 self.subprocess = subprocess
Edward Lemurb9830242019-10-30 22:19:20 +0000642 self.sys = sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000643 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000644 self.time = time
maruel@chromium.org1487d532009-06-06 00:22:57 +0000645 self.unittest = unittest
Edward Lemurb9830242019-10-30 22:19:20 +0000646 if sys.version_info.major == 2:
647 self.urllib2 = urllib2
Edward Lemur16af3562019-10-17 22:11:33 +0000648 self.urllib_request = urllib_request
649 self.urllib_error = urllib_error
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000650
Robert Iannucci50258932018-03-19 10:30:59 -0700651 self.is_windows = sys.platform == 'win32'
652
Edward Lemurb9646622019-10-25 20:57:35 +0000653 # Set python_executable to 'vpython' in order to allow scripts in other
654 # repos (e.g. src.git) to automatically pick up that repo's .vpython file,
655 # instead of inheriting the one in depot_tools.
656 self.python_executable = 'vpython'
Erik Staab69135d12021-05-14 22:31:57 +0000657 # Offer a python 3 executable for use during the migration off of python 2.
658 self.python3_executable = 'vpython3'
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000659 self.environ = os.environ
660
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000661 # InputApi.platform is the platform you're currently running on.
662 self.platform = sys.platform
663
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000664 self.cpu_count = multiprocessing.cpu_count()
Bruce Dawson8254f062022-06-17 17:33:08 +0000665 if self.is_windows:
666 # TODO(crbug.com/1190269) - we can't use more than 56 child processes on
667 # Windows or Python3 may hang.
668 self.cpu_count = min(self.cpu_count, 56)
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000669
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000670 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000671 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000672
673 # We carry the canned checks so presubmit scripts can easily use them.
674 self.canned_checks = presubmit_canned_checks
675
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100676 # Temporary files we must manually remove at the end of a run.
677 self._named_temporary_files = []
Jochen Eisinger72606f82017-04-04 10:44:18 +0200678
Edward Lesmesdc11c6b2021-03-19 19:22:05 +0000679 self.owners_client = None
680 if self.gerrit:
Bruce Dawson9b9f4512022-04-27 00:56:52 +0000681 try:
682 self.owners_client = owners_client.GetCodeOwnersClient(
683 root=change.RepositoryRoot(),
684 upstream=change.UpstreamBranch(),
685 host=self.gerrit.host,
686 project=self.gerrit.project,
687 branch=self.gerrit.branch)
688 except Exception as e:
689 print('Failed to set owners_client - %s' % str(e))
Edward Lesmes9ce03f82021-01-12 20:13:31 +0000690 self.owners_db = owners_db.Database(
691 change.RepositoryRoot(), fopen=open, os_path=self.os_path)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200692 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000693 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000694 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000695
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000696 # Replace <hash_map> and <hash_set> as headers that need to be included
Edward Lemura5799e32020-01-17 19:26:51 +0000697 # with 'base/containers/hash_tables.h' instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000698 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800699 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000700 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000701 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000702 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
703 for (a, b, header) in cpplint._re_pattern_templates
704 ]
705
Edward Lemurecc27072020-01-06 16:42:34 +0000706 def SetTimeout(self, timeout):
707 self.thread_pool.timeout = timeout
708
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709 def PresubmitLocalPath(self):
710 """Returns the local path of the presubmit script currently being run.
711
712 This is useful if you don't want to hard-code absolute paths in the
713 presubmit script. For example, It can be used to find another file
714 relative to the PRESUBMIT.py script, so the whole tree can be branched and
715 the presubmit script still works, without editing its content.
716 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000717 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000718
agable0b65e732016-11-22 09:25:46 -0800719 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000720 """Same as input_api.change.AffectedFiles() except only lists files
721 (and optionally directories) in the same directory as the current presubmit
Bruce Dawson7a0b07a2020-04-23 17:14:40 +0000722 script, or subdirectories thereof. Note that files are listed using the OS
723 path separator, so backslashes are used as separators on Windows.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000724 """
Bruce Dawson31bfd512022-05-10 23:19:39 +0000725 dir_with_slash = normpath(self.PresubmitLocalPath())
726 # normpath strips trailing path separators, so the trailing separator has to
727 # be added after the normpath call.
728 if len(dir_with_slash) > 0:
729 dir_with_slash += os.path.sep
sail@chromium.org5538e022011-05-12 17:53:16 +0000730
Edward Lemurb9830242019-10-30 22:19:20 +0000731 return list(filter(
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000732 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
Edward Lemurb9830242019-10-30 22:19:20 +0000733 self.change.AffectedFiles(include_deletes, file_filter)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000734
agable0b65e732016-11-22 09:25:46 -0800735 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000736 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800737 paths = [af.LocalPath() for af in self.AffectedFiles()]
Edward Lemura5799e32020-01-17 19:26:51 +0000738 logging.debug('LocalPaths: %s', paths)
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000739 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000740
agable0b65e732016-11-22 09:25:46 -0800741 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800743 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000744
John Budorick16162372018-04-18 10:39:53 -0700745 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
agable0b65e732016-11-22 09:25:46 -0800746 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000747 in the same directory as the current presubmit script, or subdirectories
748 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000749 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000750 if include_deletes is not None:
Edward Lemura5799e32020-01-17 19:26:51 +0000751 warn('AffectedTestableFiles(include_deletes=%s)'
752 ' is deprecated and ignored' % str(include_deletes),
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000753 category=DeprecationWarning,
754 stacklevel=2)
Josip Sokcevic4de5dea2022-03-23 21:15:14 +0000755 # pylint: disable=consider-using-generator
756 return [
757 x for x in self.AffectedFiles(include_deletes=False, **kwargs)
758 if x.IsTestableFile()
759 ]
agable0b65e732016-11-22 09:25:46 -0800760
761 def AffectedTextFiles(self, include_deletes=None):
762 """An alias to AffectedTestableFiles for backwards compatibility."""
763 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000764
Josip Sokcevic8c955952021-02-01 21:32:57 +0000765 def FilterSourceFile(self,
766 affected_file,
767 files_to_check=None,
768 files_to_skip=None,
769 allow_list=None,
770 block_list=None):
Edward Lemura5799e32020-01-17 19:26:51 +0000771 """Filters out files that aren't considered 'source file'.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000772
local_bot64021412020-07-08 21:05:39 +0000773 If files_to_check or files_to_skip is None, InputApi.DEFAULT_FILES_TO_CHECK
774 and InputApi.DEFAULT_FILES_TO_SKIP is used respectively.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000775
776 The lists will be compiled as regular expression and
777 AffectedFile.LocalPath() needs to pass both list.
778
779 Note: Copy-paste this function to suit your needs or use a lambda function.
780 """
local_bot64021412020-07-08 21:05:39 +0000781 if files_to_check is None:
782 files_to_check = self.DEFAULT_FILES_TO_CHECK
783 if files_to_skip is None:
784 files_to_skip = self.DEFAULT_FILES_TO_SKIP
local_bot30f774e2020-06-25 18:23:34 +0000785
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000786 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000787 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000788 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000789 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000790 return True
Bruce Dawsona3a014e2022-04-27 23:28:17 +0000791 # Handle the cases where the files regex only handles /, but the local
792 # path uses \.
793 if self.is_windows and self.re.match(item, local_path.replace(
794 '\\', '/')):
795 return True
maruel@chromium.org3410d912009-06-09 20:56:16 +0000796 return False
local_bot64021412020-07-08 21:05:39 +0000797 return (Find(affected_file, files_to_check) and
798 not Find(affected_file, files_to_skip))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000799
800 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800801 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000802
803 If source_file is None, InputApi.FilterSourceFile() is used.
804 """
805 if not source_file:
806 source_file = self.FilterSourceFile
Edward Lemurb9830242019-10-30 22:19:20 +0000807 return list(filter(source_file, self.AffectedTestableFiles()))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000808
809 def RightHandSideLines(self, source_file_filter=None):
Edward Lemura5799e32020-01-17 19:26:51 +0000810 """An iterator over all text lines in 'new' version of changed files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000811
812 Only lists lines from new or modified text files in the change that are
813 contained by the directory of the currently executing presubmit script.
814
815 This is useful for doing line-by-line regex checks, like checking for
816 trailing whitespace.
817
818 Yields:
819 a 3 tuple:
820 the AffectedFile instance of the current file;
821 integer line number (1-based); and
822 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000823
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000824 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000825 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000826 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000827 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000828
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000829 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000830 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000831
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000832 Deny reading anything outside the repository.
833 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000834 if isinstance(file_item, AffectedFile):
835 file_item = file_item.AbsoluteLocalPath()
836 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000837 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000838 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000839
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100840 def CreateTemporaryFile(self, **kwargs):
841 """Returns a named temporary file that must be removed with a call to
842 RemoveTemporaryFiles().
843
844 All keyword arguments are forwarded to tempfile.NamedTemporaryFile(),
845 except for |delete|, which is always set to False.
846
847 Presubmit checks that need to create a temporary file and pass it for
848 reading should use this function instead of NamedTemporaryFile(), as
849 Windows fails to open a file that is already open for writing.
850
851 with input_api.CreateTemporaryFile() as f:
852 f.write('xyz')
853 f.close()
854 input_api.subprocess.check_output(['script-that', '--reads-from',
855 f.name])
856
857
858 Note that callers of CreateTemporaryFile() should not worry about removing
859 any temporary file; this is done transparently by the presubmit handling
860 code.
861 """
862 if 'delete' in kwargs:
863 # Prevent users from passing |delete|; we take care of file deletion
864 # ourselves and this prevents unintuitive error messages when we pass
865 # delete=False and 'delete' is also in kwargs.
866 raise TypeError('CreateTemporaryFile() does not take a "delete" '
867 'argument, file deletion is handled automatically by '
868 'the same presubmit_support code that creates InputApi '
869 'objects.')
870 temp_file = self.tempfile.NamedTemporaryFile(delete=False, **kwargs)
871 self._named_temporary_files.append(temp_file.name)
872 return temp_file
873
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000874 @property
875 def tbr(self):
876 """Returns if a change is TBR'ed."""
Jeremy Romandce22502017-06-20 15:37:29 -0400877 return 'TBR' in self.change.tags or self.change.TBRsFromDescription()
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000878
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000879 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000880 tests = []
881 msgs = []
882 for t in tests_mix:
Edward Lesmes8e282792018-04-03 18:50:29 -0400883 if isinstance(t, OutputApi.PresubmitResult) and t:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000884 msgs.append(t)
885 else:
886 assert issubclass(t.message, _PresubmitResult)
887 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000888 if self.verbose:
889 t.info = _PresubmitNotifyResult
Edward Lemur1037c742018-05-01 18:56:04 -0400890 if not t.kwargs.get('cwd'):
891 t.kwargs['cwd'] = self.PresubmitLocalPath()
Edward Lesmes8e282792018-04-03 18:50:29 -0400892 self.thread_pool.AddTests(tests, parallel)
Edward Lemur21000eb2019-05-24 23:25:58 +0000893 # When self.parallel is True (i.e. --parallel is passed as an option)
894 # RunTests doesn't actually run tests. It adds them to a ThreadPool that
895 # will run all tests once all PRESUBMIT files are processed.
896 # Otherwise, it will run them and return the results.
897 if not self.parallel:
Edward Lesmes8e282792018-04-03 18:50:29 -0400898 msgs.extend(self.thread_pool.RunAsync())
899 return msgs
scottmg86099d72016-09-01 09:16:51 -0700900
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000901
nick@chromium.orgff526192013-06-10 19:30:26 +0000902class _DiffCache(object):
903 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000904 def __init__(self, upstream=None):
905 """Stores the upstream revision against which all diffs will be computed."""
906 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000907
908 def GetDiff(self, path, local_root):
909 """Get the diff for a particular path."""
910 raise NotImplementedError()
911
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700912 def GetOldContents(self, path, local_root):
913 """Get the old version for a particular path."""
914 raise NotImplementedError()
915
nick@chromium.orgff526192013-06-10 19:30:26 +0000916
nick@chromium.orgff526192013-06-10 19:30:26 +0000917class _GitDiffCache(_DiffCache):
918 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000919 def __init__(self, upstream):
920 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000921 self._diffs_by_file = None
922
923 def GetDiff(self, path, local_root):
Bruce Dawson5f63d3c2022-04-19 17:02:36 +0000924 # Compare against None to distinguish between None and an initialized but
925 # empty dictionary.
926 if self._diffs_by_file == None:
nick@chromium.orgff526192013-06-10 19:30:26 +0000927 # Compute a single diff for all files and parse the output; should
928 # with git this is much faster than computing one diff for each file.
929 diffs = {}
930
931 # Don't specify any filenames below, because there are command line length
932 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000933 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
934 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000935
936 # This regex matches the path twice, separated by a space. Note that
937 # filename itself may contain spaces.
938 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
939 current_diff = []
940 keep_line_endings = True
941 for x in unified_diff.splitlines(keep_line_endings):
942 match = file_marker.match(x)
943 if match:
944 # Marks the start of a new per-file section.
945 diffs[match.group('filename')] = current_diff = [x]
946 elif x.startswith('diff --git'):
947 raise PresubmitFailure('Unexpected diff line: %s' % x)
948 else:
949 current_diff.append(x)
950
951 self._diffs_by_file = dict(
952 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
953
954 if path not in self._diffs_by_file:
Josip Sokcevicc1ab7342022-02-24 19:02:44 +0000955 # SCM didn't have any diff on this file. It could be that the file was not
956 # modified at all (e.g. user used --all flag in git cl presubmit).
957 # Intead of failing, return empty string.
958 # See: https://crbug.com/808346.
Josip Sokcevicc1ab7342022-02-24 19:02:44 +0000959 return ''
nick@chromium.orgff526192013-06-10 19:30:26 +0000960
961 return self._diffs_by_file[path]
962
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700963 def GetOldContents(self, path, local_root):
964 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
965
nick@chromium.orgff526192013-06-10 19:30:26 +0000966
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000967class AffectedFile(object):
968 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000969
970 DIFF_CACHE = _DiffCache
971
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000972 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800973 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000974 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000975 self._path = path
976 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000977 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000978 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000979 self._cached_changed_contents = None
980 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000981 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700982 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000983
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984 def LocalPath(self):
985 """Returns the path of this file on the local disk relative to client root.
Andrew Grieve92b8b992017-11-02 09:42:24 -0400986
987 This should be used for error messages but not for accessing files,
988 because presubmit checks are run with CWD=PresubmitLocalPath() (which is
989 often != client root).
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000990 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000991 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000992
993 def AbsoluteLocalPath(self):
994 """Returns the absolute path of this file on the local disk.
995 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000996 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000997
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998 def Action(self):
999 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +00001000 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001001
agable0b65e732016-11-22 09:25:46 -08001002 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +00001003 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001004
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +00001005 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +00001006 raise NotImplementedError() # Implement when needed
1007
agable0b65e732016-11-22 09:25:46 -08001008 def IsTextFile(self):
1009 """An alias to IsTestableFile for backwards compatibility."""
1010 return self.IsTestableFile()
1011
Daniel Cheng7a1f04d2017-03-21 19:12:31 -07001012 def OldContents(self):
1013 """Returns an iterator over the lines in the old version of file.
1014
Daniel Cheng2da34fe2017-03-21 20:42:12 -07001015 The old version is the file before any modifications in the user's
Edward Lemura5799e32020-01-17 19:26:51 +00001016 workspace, i.e. the 'left hand side'.
Daniel Cheng7a1f04d2017-03-21 19:12:31 -07001017
1018 Contents will be empty if the file is a directory or does not exist.
1019 Note: The carriage returns (LF or CR) are stripped off.
1020 """
1021 return self._diff_cache.GetOldContents(self.LocalPath(),
1022 self._local_root).splitlines()
1023
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024 def NewContents(self):
1025 """Returns an iterator over the lines in the new version of file.
1026
Edward Lemura5799e32020-01-17 19:26:51 +00001027 The new version is the file in the user's workspace, i.e. the 'right hand
1028 side'.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001029
1030 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +00001031 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001032 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +00001033 if self._cached_new_contents is None:
1034 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -08001035 try:
1036 self._cached_new_contents = gclient_utils.FileRead(
1037 self.AbsoluteLocalPath(), 'rU').splitlines()
1038 except IOError:
1039 pass # File not found? That's fine; maybe it was deleted.
Greg Thompson30cde452021-06-01 16:38:47 +00001040 except UnicodeDecodeError as e:
Dirk Pranke6fc394f2021-05-26 23:25:14 +00001041 # log the filename since we're probably trying to read a binary
1042 # file, and shouldn't be.
1043 print('Error reading %s: %s' % (self.AbsoluteLocalPath(), e))
1044 raise
1045
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +00001046 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001047
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001048 def ChangedContents(self, keeplinebreaks=False):
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001049 """Returns a list of tuples (line number, line text) of all new lines.
1050
1051 This relies on the scm diff output describing each changed code section
1052 with a line of the form
1053
1054 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
1055 """
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001056 # Don't return cached results when line breaks are requested.
1057 if not keeplinebreaks and self._cached_changed_contents is not None:
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +00001058 return self._cached_changed_contents[:]
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001059 result = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001060 line_num = 0
1061
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001062 # The keeplinebreaks parameter to splitlines must be True or else the
1063 # CheckForWindowsLineEndings presubmit will be a NOP.
1064 for line in self.GenerateScmDiff().splitlines(keeplinebreaks):
Edward Lemurac5c55f2020-02-29 00:17:16 +00001065 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
1066 if m:
1067 line_num = int(m.groups(1)[0])
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001068 continue
1069 if line.startswith('+') and not line.startswith('++'):
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001070 result.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001071 if not line.startswith('-'):
1072 line_num += 1
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001073 # Don't cache results with line breaks.
1074 if keeplinebreaks:
1075 return result;
1076 self._cached_changed_contents = result
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +00001077 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001078
maruel@chromium.org5de13972009-06-10 18:16:06 +00001079 def __str__(self):
1080 return self.LocalPath()
1081
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001082 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +00001083 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001084
maruel@chromium.org58407af2011-04-12 23:15:57 +00001085
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001086class GitAffectedFile(AffectedFile):
1087 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +00001088 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001089 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001090
nick@chromium.orgff526192013-06-10 19:30:26 +00001091 DIFF_CACHE = _GitDiffCache
1092
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001093 def __init__(self, *args, **kwargs):
1094 AffectedFile.__init__(self, *args, **kwargs)
1095 self._server_path = None
agable0b65e732016-11-22 09:25:46 -08001096 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001097
agable0b65e732016-11-22 09:25:46 -08001098 def IsTestableFile(self):
1099 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001100 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -08001101 # A deleted file is not testable.
1102 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001103 else:
agable0b65e732016-11-22 09:25:46 -08001104 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
1105 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001106
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001107
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001108class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001109 """Describe a change.
1110
1111 Used directly by the presubmit scripts to query the current change being
1112 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001113
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001114 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +00001115 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001116 self.KEY: equivalent to tags['KEY']
1117 """
1118
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001119 _AFFECTED_FILES = AffectedFile
1120
Edward Lemura5799e32020-01-17 19:26:51 +00001121 # Matches key/value (or 'tag') lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +00001122 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001123 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001124 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001125
maruel@chromium.org58407af2011-04-12 23:15:57 +00001126 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001127 self, name, description, local_root, files, issue, patchset, author,
1128 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001129 if files is None:
1130 files = []
1131 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +00001132 # Convert root into an absolute path.
1133 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001134 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001135 self.issue = issue
1136 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +00001137 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001138
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001139 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001140 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001141 self._description_without_tags = ''
1142 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001143
maruel@chromium.orge085d812011-10-10 19:49:15 +00001144 assert all(
1145 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
1146
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001147 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001148 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +00001149 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
1150 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +00001151 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001152
Edward Lesmes9ce03f82021-01-12 20:13:31 +00001153 def UpstreamBranch(self):
1154 """Returns the upstream branch for the change."""
1155 return self._upstream
1156
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001157 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001158 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001159 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001160
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001161 def DescriptionText(self):
1162 """Returns the user-entered changelist description, minus tags.
1163
Edward Lemura5799e32020-01-17 19:26:51 +00001164 Any line in the user-provided description starting with e.g. 'FOO='
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001165 (whitespace permitted before and around) is considered a tag line. Such
1166 lines are stripped out of the description this function returns.
1167 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001168 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001169
1170 def FullDescriptionText(self):
1171 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001172 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001173
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001174 def SetDescriptionText(self, description):
1175 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001176
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001177 Also updates the list of tags."""
1178 self._full_description = description
1179
1180 # From the description text, build up a dictionary of key/value pairs
Edward Lemura5799e32020-01-17 19:26:51 +00001181 # plus the description minus all key/value or 'tag' lines.
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001182 description_without_tags = []
1183 self.tags = {}
1184 for line in self._full_description.splitlines():
1185 m = self.TAG_LINE_RE.match(line)
1186 if m:
1187 self.tags[m.group('key')] = m.group('value')
1188 else:
1189 description_without_tags.append(line)
1190
1191 # Change back to text and remove whitespace at end.
1192 self._description_without_tags = (
1193 '\n'.join(description_without_tags).rstrip())
1194
Edward Lemur69bb8be2020-02-03 20:37:38 +00001195 def AddDescriptionFooter(self, key, value):
1196 """Adds the given footer to the change description.
1197
1198 Args:
1199 key: A string with the key for the git footer. It must conform to
1200 the git footers format (i.e. 'List-Of-Tokens') and will be case
1201 normalized so that each token is title-cased.
1202 value: A string with the value for the git footer.
1203 """
1204 description = git_footers.add_footer(
1205 self.FullDescriptionText(), git_footers.normalize_name(key), value)
1206 self.SetDescriptionText(description)
1207
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001208 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001209 """Returns the repository (checkout) root directory for this change,
1210 as an absolute path.
1211 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001212 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001213
1214 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001215 """Return tags directly as attributes on the object."""
Edward Lemura5799e32020-01-17 19:26:51 +00001216 if not re.match(r'^[A-Z_]*$', attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001217 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +00001218 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001219
Edward Lemur69bb8be2020-02-03 20:37:38 +00001220 def GitFootersFromDescription(self):
1221 """Return the git footers present in the description.
1222
1223 Returns:
1224 footers: A dict of {footer: [values]} containing a multimap of the footers
1225 in the change description.
1226 """
1227 return git_footers.parse_footers(self.FullDescriptionText())
1228
Aaron Gablefc03e672017-05-15 14:09:42 -07001229 def BugsFromDescription(self):
1230 """Returns all bugs referenced in the commit description."""
Sean McAllister1e509c52021-10-25 17:54:25 +00001231 bug_tags = ['BUG', 'FIXED']
1232
1233 tags = []
1234 for tag in bug_tags:
1235 values = self.tags.get(tag)
1236 if values:
1237 tags += [value.strip() for value in values.split(',')]
1238
Caleb Rouleauc0546b92019-02-22 06:12:57 +00001239 footers = []
Edward Lemur69bb8be2020-02-03 20:37:38 +00001240 parsed = self.GitFootersFromDescription()
Dan Beam62954042019-10-03 21:20:33 +00001241 unsplit_footers = parsed.get('Bug', []) + parsed.get('Fixed', [])
Caleb Rouleauc0546b92019-02-22 06:12:57 +00001242 for unsplit_footer in unsplit_footers:
1243 footers += [b.strip() for b in unsplit_footer.split(',')]
Aaron Gable12ef5012017-05-15 14:29:00 -07001244 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -07001245
1246 def ReviewersFromDescription(self):
1247 """Returns all reviewers listed in the commit description."""
Edward Lemura5799e32020-01-17 19:26:51 +00001248 # We don't support a 'R:' git-footer for reviewers; that is in metadata.
Aaron Gable12ef5012017-05-15 14:29:00 -07001249 tags = [r.strip() for r in self.tags.get('R', '').split(',') if r.strip()]
1250 return sorted(set(tags))
Aaron Gablefc03e672017-05-15 14:09:42 -07001251
1252 def TBRsFromDescription(self):
1253 """Returns all TBR reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -07001254 tags = [r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()]
Aaron Gable6e7ddb62020-05-27 22:23:29 +00001255 # TODO(crbug.com/839208): Remove support for 'Tbr:' when TBRs are
1256 # programmatically determined by self-CR+1s.
Edward Lemur69bb8be2020-02-03 20:37:38 +00001257 footers = self.GitFootersFromDescription().get('Tbr', [])
Aaron Gable12ef5012017-05-15 14:29:00 -07001258 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -07001259
Aaron Gable6e7ddb62020-05-27 22:23:29 +00001260 # TODO(crbug.com/753425): Delete these once we're sure they're unused.
Aaron Gablefc03e672017-05-15 14:09:42 -07001261 @property
1262 def BUG(self):
1263 return ','.join(self.BugsFromDescription())
1264 @property
1265 def R(self):
1266 return ','.join(self.ReviewersFromDescription())
1267 @property
1268 def TBR(self):
1269 return ','.join(self.TBRsFromDescription())
1270
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001271 def AllFiles(self, root=None):
1272 """List all files under source control in the repo."""
1273 raise NotImplementedError()
1274
agable0b65e732016-11-22 09:25:46 -08001275 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001276 """Returns a list of AffectedFile instances for all files in the change.
1277
1278 Args:
1279 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +00001280 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001281
1282 Returns:
1283 [AffectedFile(path, action), AffectedFile(path, action)]
1284 """
Edward Lemurb9830242019-10-30 22:19:20 +00001285 affected = list(filter(file_filter, self._affected_files))
sail@chromium.org5538e022011-05-12 17:53:16 +00001286
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001287 if include_deletes:
1288 return affected
Edward Lemurb9830242019-10-30 22:19:20 +00001289 return list(filter(lambda x: x.Action() != 'D', affected))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001290
John Budorick16162372018-04-18 10:39:53 -07001291 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +00001292 """Return a list of the existing text files in a change."""
1293 if include_deletes is not None:
Edward Lemura5799e32020-01-17 19:26:51 +00001294 warn('AffectedTeestableFiles(include_deletes=%s)'
1295 ' is deprecated and ignored' % str(include_deletes),
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001296 category=DeprecationWarning,
1297 stacklevel=2)
Edward Lemurb9830242019-10-30 22:19:20 +00001298 return list(filter(
1299 lambda x: x.IsTestableFile(),
1300 self.AffectedFiles(include_deletes=False, **kwargs)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001301
agable0b65e732016-11-22 09:25:46 -08001302 def AffectedTextFiles(self, include_deletes=None):
1303 """An alias to AffectedTestableFiles for backwards compatibility."""
1304 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001305
agable0b65e732016-11-22 09:25:46 -08001306 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001307 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001308 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001309
agable0b65e732016-11-22 09:25:46 -08001310 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001311 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001312 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001313
1314 def RightHandSideLines(self):
Edward Lemura5799e32020-01-17 19:26:51 +00001315 """An iterator over all text lines in 'new' version of changed files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001316
1317 Lists lines from new or modified text files in the change.
1318
1319 This is useful for doing line-by-line regex checks, like checking for
1320 trailing whitespace.
1321
1322 Yields:
1323 a 3 tuple:
1324 the AffectedFile instance of the current file;
1325 integer line number (1-based); and
1326 the contents of the line as a string.
1327 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001328 return _RightHandSideLinesImpl(
1329 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -08001330 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001331
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02001332 def OriginalOwnersFiles(self):
1333 """A map from path names of affected OWNERS files to their old content."""
1334 def owners_file_filter(f):
1335 return 'OWNERS' in os.path.split(f.LocalPath())[1]
1336 files = self.AffectedFiles(file_filter=owners_file_filter)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001337 return {f.LocalPath(): f.OldContents() for f in files}
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02001338
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001339
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001340class GitChange(Change):
1341 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001342 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001343
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001344 def AllFiles(self, root=None):
1345 """List all files under source control in the repo."""
1346 root = root or self.RepositoryRoot()
1347 return subprocess.check_output(
Aaron Gable7817f022017-12-12 09:43:17 -08001348 ['git', '-c', 'core.quotePath=false', 'ls-files', '--', '.'],
Josip Sokcevic0b123462021-06-08 20:41:32 +00001349 cwd=root).decode('utf-8', 'ignore').splitlines()
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001350
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001351
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001352def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001353 """Finds all presubmit files that apply to a given set of source files.
1354
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001355 If inherit-review-settings-ok is present right under root, looks for
1356 PRESUBMIT.py in directories enclosing root.
1357
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001358 Args:
1359 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001360 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001361
1362 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001363 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001364 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001365 files = [normpath(os.path.join(root, f)) for f in files]
1366
1367 # List all the individual directories containing files.
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001368 directories = {os.path.dirname(f) for f in files}
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001369
1370 # Ignore root if inherit-review-settings-ok is present.
1371 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1372 root = None
1373
1374 # Collect all unique directories that may contain PRESUBMIT.py.
1375 candidates = set()
1376 for directory in directories:
1377 while True:
1378 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001379 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001380 candidates.add(directory)
1381 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001382 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001383 parent_dir = os.path.dirname(directory)
1384 if parent_dir == directory:
1385 # We hit the system root directory.
1386 break
1387 directory = parent_dir
1388
1389 # Look for PRESUBMIT.py in all candidate directories.
1390 results = []
1391 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -07001392 try:
1393 for f in os.listdir(directory):
1394 p = os.path.join(directory, f)
1395 if os.path.isfile(p) and re.match(
1396 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1397 results.append(p)
1398 except OSError:
1399 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001400
tobiasjs2836bcf2016-08-16 04:08:16 -07001401 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001402 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001403
1404
rmistry@google.com5626a922015-02-26 14:03:30 +00001405class GetPostUploadExecuter(object):
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001406 def __init__(self, use_python3):
1407 """
1408 Args:
1409 use_python3: if true, will use python3 instead of python2 by default
1410 if USE_PYTHON3 is not specified.
1411 """
1412 self.use_python3 = use_python3
1413
1414 def ExecPresubmitScript(self, script_text, presubmit_path, gerrit_obj,
1415 change):
rmistry@google.com5626a922015-02-26 14:03:30 +00001416 """Executes PostUploadHook() from a single presubmit script.
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001417 Caller is responsible for validating whether the hook should be executed
1418 and should only call this function if it should be.
rmistry@google.com5626a922015-02-26 14:03:30 +00001419
1420 Args:
1421 script_text: The text of the presubmit script.
1422 presubmit_path: Project script to run.
Edward Lemur016a0872020-02-04 22:13:28 +00001423 gerrit_obj: The GerritAccessor object.
rmistry@google.com5626a922015-02-26 14:03:30 +00001424 change: The Change object.
1425
1426 Return:
1427 A list of results objects.
1428 """
1429 context = {}
1430 try:
Raul Tambre09e64b42019-05-14 01:57:22 +00001431 exec(compile(script_text, 'PRESUBMIT.py', 'exec', dont_inherit=True),
1432 context)
Raul Tambre7c938462019-05-24 16:35:35 +00001433 except Exception as e:
rmistry@google.com5626a922015-02-26 14:03:30 +00001434 raise PresubmitFailure('"%s" had an exception.\n%s'
1435 % (presubmit_path, e))
1436
1437 function_name = 'PostUploadHook'
1438 if function_name not in context:
1439 return {}
1440 post_upload_hook = context[function_name]
1441 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1442 raise PresubmitFailure(
1443 'Expected function "PostUploadHook" to take three arguments.')
Edward Lemur016a0872020-02-04 22:13:28 +00001444 return post_upload_hook(gerrit_obj, change, OutputApi(False))
rmistry@google.com5626a922015-02-26 14:03:30 +00001445
1446
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001447def _MergeMasters(masters1, masters2):
1448 """Merges two master maps. Merges also the tests of each builder."""
1449 result = {}
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001450 for (master, builders) in itertools.chain(masters1.items(),
1451 masters2.items()):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001452 new_builders = result.setdefault(master, {})
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001453 for (builder, tests) in builders.items():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001454 new_builders.setdefault(builder, set([])).update(tests)
1455 return result
1456
1457
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001458def DoPostUploadExecuter(change, gerrit_obj, verbose, use_python3=False):
rmistry@google.com5626a922015-02-26 14:03:30 +00001459 """Execute the post upload hook.
1460
1461 Args:
1462 change: The Change object.
Edward Lemur016a0872020-02-04 22:13:28 +00001463 gerrit_obj: The GerritAccessor object.
rmistry@google.com5626a922015-02-26 14:03:30 +00001464 verbose: Prints debug info.
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001465 use_python3: if true, default to using Python3 for presubmit checks
1466 rather than Python2.
rmistry@google.com5626a922015-02-26 14:03:30 +00001467 """
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001468 python_version = 'Python %s' % sys.version_info.major
1469 sys.stdout.write('Running %s post upload checks ...\n' % python_version)
rmistry@google.com5626a922015-02-26 14:03:30 +00001470 presubmit_files = ListRelevantPresubmitFiles(
Edward Lemur6eb1d322020-02-27 22:20:15 +00001471 change.LocalPaths(), change.RepositoryRoot())
rmistry@google.com5626a922015-02-26 14:03:30 +00001472 if not presubmit_files and verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001473 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
rmistry@google.com5626a922015-02-26 14:03:30 +00001474 results = []
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001475 executer = GetPostUploadExecuter(use_python3)
rmistry@google.com5626a922015-02-26 14:03:30 +00001476 # The root presubmit file should be executed after the ones in subdirectories.
1477 # i.e. the specific post upload hooks should run before the general ones.
1478 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1479 presubmit_files.reverse()
1480
1481 for filename in presubmit_files:
1482 filename = os.path.abspath(filename)
1483 if verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001484 sys.stdout.write('Running %s\n' % filename)
rmistry@google.com5626a922015-02-26 14:03:30 +00001485 # Accept CRLF presubmit script.
Bruce Dawson6e70e862022-07-01 19:37:01 +00001486 presubmit_script = gclient_utils.FileRead(filename).replace('\r\n', '\n')
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001487 if _ShouldRunPresubmit(presubmit_script, use_python3):
1488 results.extend(executer.ExecPresubmitScript(
1489 presubmit_script, filename, gerrit_obj, change))
rmistry@google.com5626a922015-02-26 14:03:30 +00001490
Edward Lemur6eb1d322020-02-27 22:20:15 +00001491 if not results:
1492 return 0
1493
1494 sys.stdout.write('\n')
1495 sys.stdout.write('** Post Upload Hook Messages **\n')
1496
1497 exit_code = 0
1498 for result in results:
1499 if result.fatal:
1500 exit_code = 1
1501 result.handle()
1502 sys.stdout.write('\n')
1503
1504 return exit_code
rmistry@google.com5626a922015-02-26 14:03:30 +00001505
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001506class PresubmitExecuter(object):
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001507 def __init__(self, change, committing, verbose, gerrit_obj, dry_run=None,
Bruce Dawson09c0c072022-05-26 20:28:58 +00001508 thread_pool=None, parallel=False, use_python3=False,
1509 no_diffs=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001510 """
1511 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001512 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001513 committing: True if 'git cl land' is running, False if 'git cl upload' is.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001514 gerrit_obj: provides basic Gerrit codereview functionality.
1515 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001516 parallel: if true, all tests reported via input_api.RunTests for all
1517 PRESUBMIT files will be run in parallel.
Dirk Pranke6f0df682021-06-25 00:42:33 +00001518 use_python3: if true, will use python3 instead of python2 by default
1519 if USE_PYTHON3 is not specified.
Bruce Dawson09c0c072022-05-26 20:28:58 +00001520 no_diffs: if true, implies that --files or --all was specified so some
1521 checks can be skipped, and some errors will be messages.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001522 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001523 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001524 self.committing = committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001525 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001526 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001527 self.dry_run = dry_run
Daniel Cheng7227d212017-11-17 08:12:37 -08001528 self.more_cc = []
Edward Lesmes8e282792018-04-03 18:50:29 -04001529 self.thread_pool = thread_pool
1530 self.parallel = parallel
Dirk Pranke6f0df682021-06-25 00:42:33 +00001531 self.use_python3 = use_python3
Bruce Dawson09c0c072022-05-26 20:28:58 +00001532 self.no_diffs = no_diffs
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001533
1534 def ExecPresubmitScript(self, script_text, presubmit_path):
1535 """Executes a single presubmit script.
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001536 Caller is responsible for validating whether the hook should be executed
1537 and should only call this function if it should be.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001538
1539 Args:
1540 script_text: The text of the presubmit script.
1541 presubmit_path: The path to the presubmit file (this will be reported via
1542 input_api.PresubmitLocalPath()).
1543
1544 Return:
1545 A list of result objects, empty if no problems.
1546 """
chase@chromium.org8e416c82009-10-06 04:30:44 +00001547 # Change to the presubmit file's directory to support local imports.
1548 main_path = os.getcwd()
Saagar Sanghavi98b332f2020-07-31 17:19:15 +00001549 presubmit_dir = os.path.dirname(presubmit_path)
1550 os.chdir(presubmit_dir)
chase@chromium.org8e416c82009-10-06 04:30:44 +00001551
1552 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001553 input_api = InputApi(self.change, presubmit_path, self.committing,
Aaron Gable668c1d82018-04-03 10:19:16 -07001554 self.verbose, gerrit_obj=self.gerrit,
Edward Lesmes8e282792018-04-03 18:50:29 -04001555 dry_run=self.dry_run, thread_pool=self.thread_pool,
Bruce Dawson09c0c072022-05-26 20:28:58 +00001556 parallel=self.parallel, no_diffs=self.no_diffs)
Daniel Cheng7227d212017-11-17 08:12:37 -08001557 output_api = OutputApi(self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001558 context = {}
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001559
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001560 try:
Raul Tambre09e64b42019-05-14 01:57:22 +00001561 exec(compile(script_text, 'PRESUBMIT.py', 'exec', dont_inherit=True),
1562 context)
Raul Tambre7c938462019-05-24 16:35:35 +00001563 except Exception as e:
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001564 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001565
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001566 context['__args'] = (input_api, output_api)
Ben Pastene8351dc12020-08-06 05:01:35 +00001567
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001568 # Get path of presubmit directory relative to repository root.
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001569 # Always use forward slashes, so that path is same in *nix and Windows
1570 root = input_api.change.RepositoryRoot()
1571 rel_path = os.path.relpath(presubmit_dir, root)
1572 rel_path = rel_path.replace(os.path.sep, '/')
Ben Pastene8351dc12020-08-06 05:01:35 +00001573
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001574 # Get the URL of git remote origin and use it to identify host and project
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001575 host = project = ''
1576 if self.gerrit:
1577 host = self.gerrit.host or ''
1578 project = self.gerrit.project or ''
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001579
1580 # Prefix for test names
1581 prefix = 'presubmit:%s/%s:%s/' % (host, project, rel_path)
1582
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001583 # Perform all the desired presubmit checks.
1584 results = []
Ben Pastene8351dc12020-08-06 05:01:35 +00001585
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001586 try:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001587 version = [
1588 int(x) for x in context.get('PRESUBMIT_VERSION', '0.0.0').split('.')
1589 ]
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001590
Scott Leecc2fe9b2020-11-19 19:38:06 +00001591 with rdb_wrapper.client(prefix) as sink:
1592 if version >= [2, 0, 0]:
Andrew Grievebdfb8f22022-02-02 21:56:47 +00001593 # Copy the keys to prevent "dictionary changed size during iteration"
1594 # exception if checks add globals to context. E.g. sometimes the
1595 # Python runtime will add __warningregistry__.
1596 for function_name in list(context.keys()):
Scott Leecc2fe9b2020-11-19 19:38:06 +00001597 if not function_name.startswith('Check'):
1598 continue
1599 if function_name.endswith('Commit') and not self.committing:
1600 continue
1601 if function_name.endswith('Upload') and self.committing:
1602 continue
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001603 logging.debug('Running %s in %s', function_name, presubmit_path)
1604 results.extend(
Bruce Dawson8fa42e22022-03-29 17:05:38 +00001605 self._run_check_function(function_name, context, sink,
1606 presubmit_path))
Scott Leecc2fe9b2020-11-19 19:38:06 +00001607 logging.debug('Running %s done.', function_name)
1608 self.more_cc.extend(output_api.more_cc)
1609
1610 else: # Old format
1611 if self.committing:
1612 function_name = 'CheckChangeOnCommit'
1613 else:
1614 function_name = 'CheckChangeOnUpload'
Andrew Grievebdfb8f22022-02-02 21:56:47 +00001615 if function_name in list(context.keys()):
Scott Leecc2fe9b2020-11-19 19:38:06 +00001616 logging.debug('Running %s in %s', function_name, presubmit_path)
1617 results.extend(
Bruce Dawson8fa42e22022-03-29 17:05:38 +00001618 self._run_check_function(function_name, context, sink,
1619 presubmit_path))
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001620 logging.debug('Running %s done.', function_name)
1621 self.more_cc.extend(output_api.more_cc)
1622
1623 finally:
1624 for f in input_api._named_temporary_files:
1625 os.remove(f)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001626
chase@chromium.org8e416c82009-10-06 04:30:44 +00001627 # Return the process to the original working directory.
1628 os.chdir(main_path)
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001629 return results
1630
Bruce Dawson8fa42e22022-03-29 17:05:38 +00001631 def _run_check_function(self, function_name, context, sink, presubmit_path):
Scott Leecc2fe9b2020-11-19 19:38:06 +00001632 """Evaluates and returns the result of a given presubmit function.
1633
1634 If sink is given, the result of the presubmit function will be reported
1635 to the ResultSink.
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001636
1637 Args:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001638 function_name: the name of the presubmit function to evaluate
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001639 context: a context dictionary in which the function will be evaluated
Scott Leecc2fe9b2020-11-19 19:38:06 +00001640 sink: an instance of ResultSink. None, by default.
1641 Returns:
1642 the result of the presubmit function call.
1643 """
1644 start_time = time_time()
1645 try:
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001646 result = eval(function_name + '(*__args)', context)
1647 self._check_result_type(result)
Allen Webbfe7d7092021-05-18 02:05:49 +00001648 except Exception:
Bruce Dawson10a82862022-05-27 19:25:56 +00001649 _, e_value, _ = sys.exc_info()
1650 result = [
1651 OutputApi.PresubmitError(
1652 'Evaluation of %s failed: %s, %s' %
1653 (function_name, e_value, traceback.format_exc()))
1654 ]
Scott Leecc2fe9b2020-11-19 19:38:06 +00001655
Bruce Dawson2bdc49f2021-05-21 19:13:03 +00001656 elapsed_time = time_time() - start_time
1657 if elapsed_time > 10.0:
Bruce Dawson6757d462022-07-13 04:04:40 +00001658 sys.stdout.write('%6.1fs to run %s from %s.\n' %
1659 (elapsed_time, function_name, presubmit_path))
Scott Leecc2fe9b2020-11-19 19:38:06 +00001660 if sink:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001661 status = rdb_wrapper.STATUS_PASS
1662 if any(r.fatal for r in result):
1663 status = rdb_wrapper.STATUS_FAIL
1664 sink.report(function_name, status, elapsed_time)
1665
1666 return result
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001667
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001668 def _check_result_type(self, result):
1669 """Helper function which ensures result is a list, and all elements are
1670 instances of OutputApi.PresubmitResult"""
1671 if not isinstance(result, (tuple, list)):
1672 raise PresubmitFailure('Presubmit functions must return a tuple or list')
1673 if not all(isinstance(res, OutputApi.PresubmitResult) for res in result):
1674 raise PresubmitFailure(
1675 'All presubmit results must be of types derived from '
1676 'output_api.PresubmitResult')
1677
1678
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001679def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001680 committing,
1681 verbose,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001682 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001683 may_prompt,
Aaron Gable668c1d82018-04-03 10:19:16 -07001684 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04001685 dry_run=None,
Debrian Figueroadd2737e2019-06-21 23:50:13 +00001686 parallel=False,
Dirk Pranke6f0df682021-06-25 00:42:33 +00001687 json_output=None,
Bruce Dawson09c0c072022-05-26 20:28:58 +00001688 use_python3=False,
1689 no_diffs=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001690 """Runs all presubmit checks that apply to the files in the change.
1691
1692 This finds all PRESUBMIT.py files in directories enclosing the files in the
1693 change (up to the repository root) and calls the relevant entrypoint function
1694 depending on whether the change is being committed or uploaded.
1695
1696 Prints errors, warnings and notifications. Prompts the user for warnings
1697 when needed.
1698
1699 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001700 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001701 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001702 verbose: Prints debug info.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001703 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001704 may_prompt: Enable (y/n) questions on warning or error. If False,
1705 any questions are answered with yes by default.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001706 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001707 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001708 parallel: if true, all tests specified by input_api.RunTests in all
1709 PRESUBMIT files will be run in parallel.
Dirk Pranke6f0df682021-06-25 00:42:33 +00001710 use_python3: if true, default to using Python3 for presubmit checks
1711 rather than Python2.
Bruce Dawson09c0c072022-05-26 20:28:58 +00001712 no_diffs: if true, implies that --files or --all was specified so some
1713 checks can be skipped, and some errors will be messages.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001714 Return:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001715 1 if presubmit checks failed or 0 otherwise.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001716 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001717 old_environ = os.environ
1718 try:
1719 # Make sure python subprocesses won't generate .pyc files.
1720 os.environ = os.environ.copy()
Edward Lemurb9830242019-10-30 22:19:20 +00001721 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001722
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001723 python_version = 'Python %s' % sys.version_info.major
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001724 if committing:
Dirk Pranke6fc394f2021-05-26 23:25:14 +00001725 sys.stdout.write('Running %s presubmit commit checks ...\n' %
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001726 python_version)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001727 else:
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001728 sys.stdout.write('Running %s presubmit upload checks ...\n' %
1729 python_version)
Edward Lemurecc27072020-01-06 16:42:34 +00001730 start_time = time_time()
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001731 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001732 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001733 if not presubmit_files and verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001734 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001735 results = []
Bruce Dawsonf2b04212022-06-10 16:42:54 +00001736 depot_tools = os.path.dirname(os.path.abspath(__file__))
1737 python2_usage_log_file = os.path.join(depot_tools, 'python2_usage.txt')
1738 if os.path.exists(python2_usage_log_file):
1739 os.remove(python2_usage_log_file)
Edward Lesmes8e282792018-04-03 18:50:29 -04001740 thread_pool = ThreadPool()
Edward Lemur7e3c67f2018-07-20 20:52:49 +00001741 executer = PresubmitExecuter(change, committing, verbose, gerrit_obj,
Bruce Dawson09c0c072022-05-26 20:28:58 +00001742 dry_run, thread_pool, parallel, use_python3,
1743 no_diffs)
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001744 skipped_count = 0;
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001745 if default_presubmit:
1746 if verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001747 sys.stdout.write('Running default presubmit script.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001748 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001749 if _ShouldRunPresubmit(default_presubmit, use_python3):
1750 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1751 else:
1752 skipped_count += 1
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001753 for filename in presubmit_files:
1754 filename = os.path.abspath(filename)
1755 if verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001756 sys.stdout.write('Running %s\n' % filename)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001757 # Accept CRLF presubmit script.
Bruce Dawson6e70e862022-07-01 19:37:01 +00001758 presubmit_script = gclient_utils.FileRead(filename).replace('\r\n', '\n')
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001759 if _ShouldRunPresubmit(presubmit_script, use_python3):
1760 results += executer.ExecPresubmitScript(presubmit_script, filename)
1761 else:
1762 skipped_count += 1
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001763
Edward Lesmes8e282792018-04-03 18:50:29 -04001764 results += thread_pool.RunAsync()
1765
Bruce Dawsonf2b04212022-06-10 16:42:54 +00001766 if os.path.exists(python2_usage_log_file):
1767 with open(python2_usage_log_file) as f:
1768 python2_usage = [x.strip() for x in f.readlines()]
1769 results.append(
1770 OutputApi(committing).PresubmitPromptWarning(
1771 'Python 2 scripts were run during %s presubmits. Please see '
1772 'https://bugs.chromium.org/p/chromium/issues/detail?id=1313804'
1773 '#c61 for tips on resolving this.'
1774 % python_version,
1775 items=python2_usage))
1776
Edward Lemur6eb1d322020-02-27 22:20:15 +00001777 messages = {}
1778 should_prompt = False
1779 presubmits_failed = False
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001780 for result in results:
1781 if result.fatal:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001782 presubmits_failed = True
1783 messages.setdefault('ERRORS', []).append(result)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001784 elif result.should_prompt:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001785 should_prompt = True
1786 messages.setdefault('Warnings', []).append(result)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001787 else:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001788 messages.setdefault('Messages', []).append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001789
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001790 # Print the different message types in a consistent order. ERRORS go last
1791 # so that they will be most visible in the local-presubmit output.
1792 for name in ['Messages', 'Warnings', 'ERRORS']:
1793 if name in messages:
1794 items = messages[name]
Gavin Makd22bf602022-07-11 21:10:41 +00001795 sys.stdout.write('** Presubmit %s: %d **\n' % (name, len(items)))
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001796 for item in items:
1797 item.handle()
1798 sys.stdout.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001799
Edward Lemurecc27072020-01-06 16:42:34 +00001800 total_time = time_time() - start_time
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001801 if total_time > 1.0:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001802 sys.stdout.write(
Louis Romero4ca9e1c2022-03-10 10:29:11 +00001803 'Presubmit checks took %.1fs to calculate.\n' % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001804
Edward Lemur6eb1d322020-02-27 22:20:15 +00001805 if not should_prompt and not presubmits_failed:
Louis Romero4ca9e1c2022-03-10 10:29:11 +00001806 sys.stdout.write('%s presubmit checks passed.\n\n' % python_version)
Josip Sokcevic7592e0a2022-01-12 00:57:54 +00001807 elif should_prompt and not presubmits_failed:
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001808 sys.stdout.write('There were %s presubmit warnings. ' % python_version)
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001809 if may_prompt:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001810 presubmits_failed = not prompt_should_continue(
1811 'Are you sure you wish to continue? (y/N): ')
Garrett Beatyacb807e2020-08-28 18:17:24 +00001812 else:
1813 sys.stdout.write('\n')
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001814 else:
1815 sys.stdout.write('There were %s presubmit errors.\n' % python_version)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001816
Edward Lemur1dc66e12020-02-21 21:36:34 +00001817 if json_output:
1818 # Write the presubmit results to json output
1819 presubmit_results = {
1820 'errors': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001821 error.json_format()
1822 for error in messages.get('ERRORS', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001823 ],
1824 'notifications': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001825 notification.json_format()
1826 for notification in messages.get('Messages', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001827 ],
1828 'warnings': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001829 warning.json_format()
1830 for warning in messages.get('Warnings', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001831 ],
Edward Lemur6eb1d322020-02-27 22:20:15 +00001832 'more_cc': executer.more_cc,
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001833 'skipped_presubmits': skipped_count,
Edward Lemur1dc66e12020-02-21 21:36:34 +00001834 }
1835
1836 gclient_utils.FileWrite(
1837 json_output, json.dumps(presubmit_results, sort_keys=True))
1838
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001839 global _ASKED_FOR_FEEDBACK
1840 # Ask for feedback one time out of 5.
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001841 if (results and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
Edward Lemur6eb1d322020-02-27 22:20:15 +00001842 sys.stdout.write(
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001843 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1844 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1845 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001846 _ASKED_FOR_FEEDBACK = True
Edward Lemur6eb1d322020-02-27 22:20:15 +00001847
1848 return 1 if presubmits_failed else 0
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001849 finally:
1850 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001851
1852
Edward Lemur50984a62020-02-06 18:10:18 +00001853def _scan_sub_dirs(mask, recursive):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001854 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001855 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001856
1857 results = []
1858 for root, dirs, files in os.walk('.'):
1859 if '.svn' in dirs:
1860 dirs.remove('.svn')
1861 if '.git' in dirs:
1862 dirs.remove('.git')
1863 for name in files:
1864 if fnmatch.fnmatch(name, mask):
1865 results.append(os.path.join(root, name))
1866 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001867
1868
Edward Lemur50984a62020-02-06 18:10:18 +00001869def _parse_files(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001870 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001871 files = []
1872 for arg in args:
Edward Lemur50984a62020-02-06 18:10:18 +00001873 files.extend([('M', f) for f in _scan_sub_dirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001874 return files
1875
1876
Edward Lemur50984a62020-02-06 18:10:18 +00001877def _parse_change(parser, options):
1878 """Process change options.
1879
1880 Args:
1881 parser: The parser used to parse the arguments from command line.
1882 options: The arguments parsed from command line.
1883 Returns:
1884 A GitChange if the change root is a git repository, or a Change otherwise.
1885 """
1886 if options.files and options.all_files:
1887 parser.error('<files> cannot be specified when --all-files is set.')
1888
Edward Lesmes44ea3ff2020-02-05 00:14:30 +00001889 change_scm = scm.determine_scm(options.root)
Edward Lemur50984a62020-02-06 18:10:18 +00001890 if change_scm != 'git' and not options.files:
1891 parser.error('<files> is not optional for unversioned directories.')
1892
1893 if options.files:
Josip Sokcevic017544d2022-03-31 23:47:53 +00001894 if options.source_controlled_only:
1895 # Get the filtered set of files from SCM.
1896 change_files = []
1897 for name in scm.GIT.GetAllFiles(options.root):
1898 for mask in options.files:
1899 if fnmatch.fnmatch(name, mask):
1900 change_files.append(('M', name))
1901 break
1902 else:
1903 # Get the filtered set of files from a directory scan.
1904 change_files = _parse_files(options.files, options.recursive)
Edward Lemur50984a62020-02-06 18:10:18 +00001905 elif options.all_files:
1906 change_files = [('M', f) for f in scm.GIT.GetAllFiles(options.root)]
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001907 else:
Edward Lemur50984a62020-02-06 18:10:18 +00001908 change_files = scm.GIT.CaptureStatus(
Edward Lemur7f6dec02020-02-06 20:23:58 +00001909 options.root, options.upstream or None)
Edward Lemur50984a62020-02-06 18:10:18 +00001910
1911 logging.info('Found %d file(s).', len(change_files))
1912
1913 change_class = GitChange if change_scm == 'git' else Change
1914 return change_class(
1915 options.name,
1916 options.description,
1917 options.root,
1918 change_files,
1919 options.issue,
1920 options.patchset,
1921 options.author,
1922 upstream=options.upstream)
1923
1924
1925def _parse_gerrit_options(parser, options):
1926 """Process gerrit options.
1927
1928 SIDE EFFECTS: Modifies options.author and options.description from Gerrit if
1929 options.gerrit_fetch is set.
1930
1931 Args:
1932 parser: The parser used to parse the arguments from command line.
1933 options: The arguments parsed from command line.
1934 Returns:
Stephen Martinisfb09de22021-02-25 03:41:13 +00001935 A GerritAccessor object if options.gerrit_url is set, or None otherwise.
Edward Lemur50984a62020-02-06 18:10:18 +00001936 """
1937 gerrit_obj = None
Stephen Martinisfb09de22021-02-25 03:41:13 +00001938 if options.gerrit_url:
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001939 gerrit_obj = GerritAccessor(
1940 url=options.gerrit_url,
1941 project=options.gerrit_project,
1942 branch=options.gerrit_branch)
Edward Lemur50984a62020-02-06 18:10:18 +00001943
1944 if not options.gerrit_fetch:
1945 return gerrit_obj
1946
1947 if not options.gerrit_url or not options.issue or not options.patchset:
1948 parser.error(
1949 '--gerrit_fetch requires --gerrit_url, --issue and --patchset.')
1950
1951 options.author = gerrit_obj.GetChangeOwner(options.issue)
1952 options.description = gerrit_obj.GetChangeDescription(
1953 options.issue, options.patchset)
1954
1955 logging.info('Got author: "%s"', options.author)
1956 logging.info('Got description: """\n%s\n"""', options.description)
1957
1958 return gerrit_obj
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001959
1960
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001961@contextlib.contextmanager
1962def canned_check_filter(method_names):
1963 filtered = {}
1964 try:
1965 for method_name in method_names:
1966 if not hasattr(presubmit_canned_checks, method_name):
Gavin Make6a62332020-12-04 21:57:10 +00001967 logging.warning('Skipping unknown "canned" check %s' % method_name)
Aaron Gableecee74c2018-04-02 15:13:08 -07001968 continue
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001969 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1970 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1971 yield
1972 finally:
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001973 for name, method in filtered.items():
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001974 setattr(presubmit_canned_checks, name, method)
1975
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001976
sbc@chromium.org013731e2015-02-26 18:28:43 +00001977def main(argv=None):
Edward Lemura5799e32020-01-17 19:26:51 +00001978 parser = argparse.ArgumentParser(usage='%(prog)s [options] <files...>')
1979 hooks = parser.add_mutually_exclusive_group()
1980 hooks.add_argument('-c', '--commit', action='store_true',
1981 help='Use commit instead of upload checks.')
1982 hooks.add_argument('-u', '--upload', action='store_false', dest='commit',
1983 help='Use upload instead of commit checks.')
Edward Lemur75526302020-02-27 22:31:05 +00001984 hooks.add_argument('--post_upload', action='store_true',
1985 help='Run post-upload commit hooks.')
Edward Lemura5799e32020-01-17 19:26:51 +00001986 parser.add_argument('-r', '--recursive', action='store_true',
1987 help='Act recursively.')
1988 parser.add_argument('-v', '--verbose', action='count', default=0,
1989 help='Use 2 times for more debug info.')
1990 parser.add_argument('--name', default='no name')
1991 parser.add_argument('--author')
Edward Lemur227d5102020-02-25 23:45:35 +00001992 desc = parser.add_mutually_exclusive_group()
1993 desc.add_argument('--description', default='', help='The change description.')
1994 desc.add_argument('--description_file',
1995 help='File to read change description from.')
Edward Lemura5799e32020-01-17 19:26:51 +00001996 parser.add_argument('--issue', type=int, default=0)
1997 parser.add_argument('--patchset', type=int, default=0)
1998 parser.add_argument('--root', default=os.getcwd(),
1999 help='Search for PRESUBMIT.py up to this directory. '
2000 'If inherit-review-settings-ok is present in this '
2001 'directory, parent directories up to the root file '
2002 'system directories will also be searched.')
2003 parser.add_argument('--upstream',
2004 help='Git only: the base ref or upstream branch against '
2005 'which the diff should be computed.')
2006 parser.add_argument('--default_presubmit')
2007 parser.add_argument('--may_prompt', action='store_true', default=False)
2008 parser.add_argument('--skip_canned', action='append', default=[],
2009 help='A list of checks to skip which appear in '
2010 'presubmit_canned_checks. Can be provided multiple times '
2011 'to skip multiple canned checks.')
2012 parser.add_argument('--dry_run', action='store_true', help=argparse.SUPPRESS)
2013 parser.add_argument('--gerrit_url', help=argparse.SUPPRESS)
Edward Lesmeseb1bd622021-03-01 19:54:07 +00002014 parser.add_argument('--gerrit_project', help=argparse.SUPPRESS)
2015 parser.add_argument('--gerrit_branch', help=argparse.SUPPRESS)
Edward Lemura5799e32020-01-17 19:26:51 +00002016 parser.add_argument('--gerrit_fetch', action='store_true',
2017 help=argparse.SUPPRESS)
2018 parser.add_argument('--parallel', action='store_true',
2019 help='Run all tests specified by input_api.RunTests in '
2020 'all PRESUBMIT files in parallel.')
2021 parser.add_argument('--json_output',
2022 help='Write presubmit errors to json output.')
Edward Lemur1dc66e12020-02-21 21:36:34 +00002023 parser.add_argument('--all_files', action='store_true',
Edward Lemur50984a62020-02-06 18:10:18 +00002024 help='Mark all files under source control as modified.')
Bruce Dawson09c0c072022-05-26 20:28:58 +00002025
Edward Lemura5799e32020-01-17 19:26:51 +00002026 parser.add_argument('files', nargs='*',
2027 help='List of files to be marked as modified when '
2028 'executing presubmit or post-upload hooks. fnmatch '
2029 'wildcards can also be used.')
Josip Sokcevic017544d2022-03-31 23:47:53 +00002030 parser.add_argument('--source_controlled_only', action='store_true',
2031 help='Constrain \'files\' to those in source control.')
Dirk Pranke6f0df682021-06-25 00:42:33 +00002032 parser.add_argument('--use-python3', action='store_true',
2033 help='Use python3 for presubmit checks by default')
Bruce Dawson09c0c072022-05-26 20:28:58 +00002034 parser.add_argument('--no_diffs', action='store_true',
2035 help='Assume that all "modified" files have no diffs.')
Edward Lemura5799e32020-01-17 19:26:51 +00002036 options = parser.parse_args(argv)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00002037
Erik Staabcca5c492020-04-16 17:40:07 +00002038 log_level = logging.ERROR
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002039 if options.verbose >= 2:
Erik Staabcca5c492020-04-16 17:40:07 +00002040 log_level = logging.DEBUG
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002041 elif options.verbose:
Erik Staabcca5c492020-04-16 17:40:07 +00002042 log_level = logging.INFO
2043 log_format = ('[%(levelname).1s%(asctime)s %(process)d %(thread)d '
2044 '%(filename)s] %(message)s')
2045 logging.basicConfig(format=log_format, level=log_level)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00002046
Edward Lemur227d5102020-02-25 23:45:35 +00002047 if options.description_file:
2048 options.description = gclient_utils.FileRead(options.description_file)
Edward Lemur50984a62020-02-06 18:10:18 +00002049 gerrit_obj = _parse_gerrit_options(parser, options)
2050 change = _parse_change(parser, options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00002051
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002052 try:
Edward Lemur75526302020-02-27 22:31:05 +00002053 if options.post_upload:
Josip Sokcevice293d3d2022-02-16 22:52:15 +00002054 return DoPostUploadExecuter(change, gerrit_obj, options.verbose,
2055 options.use_python3)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002056 with canned_check_filter(options.skip_canned):
Edward Lemur6eb1d322020-02-27 22:20:15 +00002057 return DoPresubmitChecks(
Edward Lemur50984a62020-02-06 18:10:18 +00002058 change,
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002059 options.commit,
2060 options.verbose,
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002061 options.default_presubmit,
2062 options.may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002063 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04002064 options.dry_run,
Debrian Figueroadd2737e2019-06-21 23:50:13 +00002065 options.parallel,
Dirk Pranke6f0df682021-06-25 00:42:33 +00002066 options.json_output,
Bruce Dawson09c0c072022-05-26 20:28:58 +00002067 options.use_python3,
2068 options.no_diffs)
Raul Tambre7c938462019-05-24 16:35:35 +00002069 except PresubmitFailure as e:
Josip Sokcevic0399e172022-03-21 23:11:51 +00002070 import utils
Raul Tambre80ee78e2019-05-06 22:41:05 +00002071 print(e, file=sys.stderr)
2072 print('Maybe your depot_tools is out of date?', file=sys.stderr)
Josip Sokcevic0399e172022-03-21 23:11:51 +00002073 print('depot_tools version: %s' % utils.depot_tools_version(),
2074 file=sys.stderr)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002075 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002076
2077
2078if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00002079 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00002080 try:
2081 sys.exit(main())
2082 except KeyboardInterrupt:
2083 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07002084 sys.exit(2)