blob: 49f566ea8616d110b2deda9a0deb5753cb0ffa35 [file] [log] [blame]
Josip Sokcevic4de5dea2022-03-23 21:15:14 +00001#!/usr/bin/env python3
maruel@chromium.org3bbf2942012-01-10 16:52:06 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Enables directory-specific presubmit checks to run at upload and/or commit.
7"""
8
Raul Tambre80ee78e2019-05-06 22:41:05 +00009from __future__ import print_function
10
Saagar Sanghavi99816902020-08-11 22:41:25 +000011__version__ = '2.0.0'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000012
13# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
14# caching (between all different invocations of presubmit scripts for a given
15# change). We should add it as our presubmit scripts start feeling slow.
16
Edward Lemura5799e32020-01-17 19:26:51 +000017import argparse
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +090018import ast # Exposed through the API.
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +000019import contextlib
Yoshisato Yanagisawa406de132018-06-29 05:43:25 +000020import cpplint
dcheng091b7db2016-06-16 01:27:51 -070021import fnmatch # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000022import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000023import inspect
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +000024import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000025import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000026import logging
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000027import multiprocessing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000028import os # Somewhat exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000029import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000030import re # Exposed through the API.
Edward Lesmes8e282792018-04-03 18:50:29 -040031import signal
Allen Webbfe7d7092021-05-18 02:05:49 +000032import six
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000033import sys # Parts exposed through API.
34import tempfile # Exposed through the API.
Edward Lesmes8e282792018-04-03 18:50:29 -040035import threading
jam@chromium.org2a891dc2009-08-20 20:33:37 +000036import time
Edward Lemurde9e3ca2019-10-24 21:13:31 +000037import traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +000038import unittest # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000039from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000040
41# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000042import fix_encoding
Yoshisato Yanagisawa04600b42019-03-15 03:03:41 +000043import gclient_paths # Exposed through the API
44import gclient_utils
Josip Sokcevic7958e302023-03-01 23:02:21 +000045import git_footers
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000046import gerrit_util
Edward Lesmes9ce03f82021-01-12 20:13:31 +000047import owners_client
Jochen Eisinger76f5fc62017-04-07 16:27:46 +020048import owners_finder
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000049import presubmit_canned_checks
Saagar Sanghavi9949ab72020-07-20 20:56:40 +000050import rdb_wrapper
Josip Sokcevic7958e302023-03-01 23:02:21 +000051import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000052import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053
Edward Lemur16af3562019-10-17 22:11:33 +000054if sys.version_info.major == 2:
55 # TODO(1009814): Expose urllib2 only through urllib_request and urllib_error
56 import urllib2 # Exposed through the API.
57 import urlparse
58 import urllib2 as urllib_request
59 import urllib2 as urllib_error
60else:
61 import urllib.parse as urlparse
62 import urllib.request as urllib_request
63 import urllib.error as urllib_error
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000064
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000065# Ask for feedback only once in program lifetime.
66_ASKED_FOR_FEEDBACK = False
67
Bruce Dawsondca14bc2022-09-15 20:59:38 +000068# Set if super-verbose mode is requested, for tracking where presubmit messages
69# are coming from.
70_SHOW_CALLSTACKS = False
71
72
Edward Lemurecc27072020-01-06 16:42:34 +000073def time_time():
74 # Use this so that it can be mocked in tests without interfering with python
75 # system machinery.
76 return time.time()
77
78
maruel@chromium.org899e1c12011-04-07 17:03:18 +000079class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000080 pass
81
82
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000083class CommandData(object):
Bruce Dawsonb6cb9e02023-05-18 20:52:43 +000084 def __init__(self, name, cmd, kwargs, message, python3=True):
85 # The python3 argument is ignored but has to be retained because of the many
86 # callers in other repos that pass it in.
87 del python3
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000088 self.name = name
89 self.cmd = cmd
Edward Lesmes8e282792018-04-03 18:50:29 -040090 self.stdin = kwargs.get('stdin', None)
Edward Lemur2d6b67c2019-08-23 22:25:41 +000091 self.kwargs = kwargs.copy()
Edward Lesmes8e282792018-04-03 18:50:29 -040092 self.kwargs['stdout'] = subprocess.PIPE
93 self.kwargs['stderr'] = subprocess.STDOUT
94 self.kwargs['stdin'] = subprocess.PIPE
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000095 self.message = message
96 self.info = None
Bruce Dawsonb6cb9e02023-05-18 20:52:43 +000097
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000098
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000099
Edward Lesmes8e282792018-04-03 18:50:29 -0400100# Adapted from
101# https://github.com/google/gtest-parallel/blob/master/gtest_parallel.py#L37
102#
103# An object that catches SIGINT sent to the Python process and notices
104# if processes passed to wait() die by SIGINT (we need to look for
105# both of those cases, because pressing Ctrl+C can result in either
106# the main process or one of the subprocesses getting the signal).
107#
108# Before a SIGINT is seen, wait(p) will simply call p.wait() and
109# return the result. Once a SIGINT has been seen (in the main process
110# or a subprocess, including the one the current call is waiting for),
Edward Lemur9a5bb612019-09-26 02:01:52 +0000111# wait(p) will call p.terminate().
Edward Lesmes8e282792018-04-03 18:50:29 -0400112class SigintHandler(object):
Edward Lesmes8e282792018-04-03 18:50:29 -0400113 sigint_returncodes = {-signal.SIGINT, # Unix
114 -1073741510, # Windows
115 }
116 def __init__(self):
117 self.__lock = threading.Lock()
118 self.__processes = set()
119 self.__got_sigint = False
Edward Lemur9a5bb612019-09-26 02:01:52 +0000120 self.__previous_signal = signal.signal(signal.SIGINT, self.interrupt)
Edward Lesmes8e282792018-04-03 18:50:29 -0400121
122 def __on_sigint(self):
123 self.__got_sigint = True
124 while self.__processes:
125 try:
126 self.__processes.pop().terminate()
127 except OSError:
128 pass
129
Edward Lemur9a5bb612019-09-26 02:01:52 +0000130 def interrupt(self, signal_num, frame):
Edward Lesmes8e282792018-04-03 18:50:29 -0400131 with self.__lock:
132 self.__on_sigint()
Edward Lemur9a5bb612019-09-26 02:01:52 +0000133 self.__previous_signal(signal_num, frame)
Edward Lesmes8e282792018-04-03 18:50:29 -0400134
135 def got_sigint(self):
136 with self.__lock:
137 return self.__got_sigint
138
139 def wait(self, p, stdin):
140 with self.__lock:
141 if self.__got_sigint:
142 p.terminate()
143 self.__processes.add(p)
144 stdout, stderr = p.communicate(stdin)
145 code = p.returncode
146 with self.__lock:
147 self.__processes.discard(p)
148 if code in self.sigint_returncodes:
149 self.__on_sigint()
Edward Lesmes8e282792018-04-03 18:50:29 -0400150 return stdout, stderr
151
152sigint_handler = SigintHandler()
153
154
Edward Lemurecc27072020-01-06 16:42:34 +0000155class Timer(object):
156 def __init__(self, timeout, fn):
157 self.completed = False
158 self._fn = fn
159 self._timer = threading.Timer(timeout, self._onTimer) if timeout else None
160
161 def __enter__(self):
162 if self._timer:
163 self._timer.start()
164 return self
165
166 def __exit__(self, _type, _value, _traceback):
167 if self._timer:
168 self._timer.cancel()
169
170 def _onTimer(self):
171 self._fn()
172 self.completed = True
173
174
Edward Lesmes8e282792018-04-03 18:50:29 -0400175class ThreadPool(object):
Edward Lemurecc27072020-01-06 16:42:34 +0000176 def __init__(self, pool_size=None, timeout=None):
177 self.timeout = timeout
Edward Lesmes8e282792018-04-03 18:50:29 -0400178 self._pool_size = pool_size or multiprocessing.cpu_count()
Bruce Dawson8254f062022-06-17 17:33:08 +0000179 if sys.platform == 'win32':
180 # TODO(crbug.com/1190269) - we can't use more than 56 child processes on
181 # Windows or Python3 may hang.
182 self._pool_size = min(self._pool_size, 56)
Edward Lesmes8e282792018-04-03 18:50:29 -0400183 self._messages = []
184 self._messages_lock = threading.Lock()
185 self._tests = []
186 self._tests_lock = threading.Lock()
187 self._nonparallel_tests = []
188
Edward Lemurecc27072020-01-06 16:42:34 +0000189 def _GetCommand(self, test):
Bruce Dawsonb6cb9e02023-05-18 20:52:43 +0000190 vpython = 'vpython3'
Edward Lemur940c2822019-08-23 00:34:25 +0000191 if sys.platform == 'win32':
192 vpython += '.bat'
Edward Lesmes8e282792018-04-03 18:50:29 -0400193
194 cmd = test.cmd
195 if cmd[0] == 'python':
196 cmd = list(cmd)
197 cmd[0] = vpython
198 elif cmd[0].endswith('.py'):
199 cmd = [vpython] + cmd
200
Edward Lemur336e51f2019-11-14 21:42:04 +0000201 # On Windows, scripts on the current directory take precedence over PATH, so
202 # that when testing depot_tools on Windows, calling `vpython.bat` will
203 # execute the copy of vpython of the depot_tools under test instead of the
204 # one in the bot.
205 # As a workaround, we run the tests from the parent directory instead.
206 if (cmd[0] == vpython and
207 'cwd' in test.kwargs and
208 os.path.basename(test.kwargs['cwd']) == 'depot_tools'):
209 test.kwargs['cwd'] = os.path.dirname(test.kwargs['cwd'])
210 cmd[1] = os.path.join('depot_tools', cmd[1])
211
Edward Lemurecc27072020-01-06 16:42:34 +0000212 return cmd
213
214 def _RunWithTimeout(self, cmd, stdin, kwargs):
215 p = subprocess.Popen(cmd, **kwargs)
216 with Timer(self.timeout, p.terminate) as timer:
217 stdout, _ = sigint_handler.wait(p, stdin)
Josip Sokcevic6635baf2021-11-19 02:04:39 +0000218 stdout = stdout.decode('utf-8', 'ignore')
Edward Lemurecc27072020-01-06 16:42:34 +0000219 if timer.completed:
220 stdout = 'Process timed out after %ss\n%s' % (self.timeout, stdout)
Josip Sokcevic6635baf2021-11-19 02:04:39 +0000221 return p.returncode, stdout
Edward Lemurecc27072020-01-06 16:42:34 +0000222
Josip Sokcevica4b871e2023-05-18 14:27:56 +0000223 def CallCommand(self, test, show_callstack=None):
Edward Lemurecc27072020-01-06 16:42:34 +0000224 """Runs an external program.
225
Edward Lemura5799e32020-01-17 19:26:51 +0000226 This function converts invocation of .py files and invocations of 'python'
Edward Lemurecc27072020-01-06 16:42:34 +0000227 to vpython invocations.
228 """
229 cmd = self._GetCommand(test)
Edward Lesmes8e282792018-04-03 18:50:29 -0400230 try:
Edward Lemurecc27072020-01-06 16:42:34 +0000231 start = time_time()
232 returncode, stdout = self._RunWithTimeout(cmd, test.stdin, test.kwargs)
233 duration = time_time() - start
Edward Lemur7cf94382019-11-15 22:36:41 +0000234 except Exception:
Edward Lemurecc27072020-01-06 16:42:34 +0000235 duration = time_time() - start
Edward Lesmes8e282792018-04-03 18:50:29 -0400236 return test.message(
Josip Sokcevica4b871e2023-05-18 14:27:56 +0000237 '%s\n%s exec failure (%4.2fs)\n%s' %
238 (test.name, ' '.join(cmd), duration, traceback.format_exc()),
239 show_callstack=show_callstack)
Edward Lemurde9e3ca2019-10-24 21:13:31 +0000240
Edward Lemurecc27072020-01-06 16:42:34 +0000241 if returncode != 0:
Josip Sokcevica4b871e2023-05-18 14:27:56 +0000242 return test.message('%s\n%s (%4.2fs) failed\n%s' %
243 (test.name, ' '.join(cmd), duration, stdout),
244 show_callstack=show_callstack)
Edward Lemurecc27072020-01-06 16:42:34 +0000245
Edward Lesmes8e282792018-04-03 18:50:29 -0400246 if test.info:
Josip Sokcevica4b871e2023-05-18 14:27:56 +0000247 return test.info('%s\n%s (%4.2fs)' % (test.name, ' '.join(cmd), duration),
248 show_callstack=show_callstack)
Edward Lesmes8e282792018-04-03 18:50:29 -0400249
250 def AddTests(self, tests, parallel=True):
251 if parallel:
252 self._tests.extend(tests)
253 else:
254 self._nonparallel_tests.extend(tests)
255
256 def RunAsync(self):
257 self._messages = []
258
259 def _WorkerFn():
260 while True:
261 test = None
262 with self._tests_lock:
263 if not self._tests:
264 break
265 test = self._tests.pop()
Josip Sokcevica4b871e2023-05-18 14:27:56 +0000266 result = self.CallCommand(test, show_callstack=False)
Edward Lesmes8e282792018-04-03 18:50:29 -0400267 if result:
268 with self._messages_lock:
269 self._messages.append(result)
270
271 def _StartDaemon():
272 t = threading.Thread(target=_WorkerFn)
273 t.daemon = True
274 t.start()
275 return t
276
277 while self._nonparallel_tests:
278 test = self._nonparallel_tests.pop()
279 result = self.CallCommand(test)
280 if result:
281 self._messages.append(result)
282
283 if self._tests:
284 threads = [_StartDaemon() for _ in range(self._pool_size)]
285 for worker in threads:
286 worker.join()
287
288 return self._messages
289
290
Josip Sokcevica9a7eec2023-03-10 03:54:52 +0000291def normpath(path):
292 '''Version of os.path.normpath that also changes backward slashes to
293 forward slashes when not running on Windows.
294 '''
295 # This is safe to always do because the Windows version of os.path.normpath
296 # will replace forward slashes with backward slashes.
297 path = path.replace(os.sep, '/')
298 return os.path.normpath(path)
299
300
Josip Sokcevic7958e302023-03-01 23:02:21 +0000301def _RightHandSideLinesImpl(affected_files):
302 """Implements RightHandSideLines for InputApi and GclChange."""
303 for af in affected_files:
304 lines = af.ChangedContents()
305 for line in lines:
306 yield (af, line[0], line[1])
307
308
Edward Lemur6eb1d322020-02-27 22:20:15 +0000309def prompt_should_continue(prompt_string):
310 sys.stdout.write(prompt_string)
Dirk Pranke7288f882021-06-03 18:01:30 +0000311 sys.stdout.flush()
Edward Lemur6eb1d322020-02-27 22:20:15 +0000312 response = sys.stdin.readline().strip().lower()
313 return response in ('y', 'yes')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000314
Josip Sokcevic967cf672023-05-10 17:09:58 +0000315
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000316# Top level object so multiprocessing can pickle
317# Public access through OutputApi object.
318class _PresubmitResult(object):
319 """Base class for result objects."""
320 fatal = False
321 should_prompt = False
322
Josip Sokcevica4b871e2023-05-18 14:27:56 +0000323 def __init__(self, message, items=None, long_text='', show_callstack=None):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000324 """
325 message: A short one-line message to indicate errors.
326 items: A list of short strings to indicate where errors occurred.
327 long_text: multi-line text output, e.g. from another tool
328 """
Bruce Dawsondb8622b2022-04-03 15:38:12 +0000329 self._message = _PresubmitResult._ensure_str(message)
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000330 self._items = items or []
Tom McKee61c72652021-07-20 11:56:32 +0000331 self._long_text = _PresubmitResult._ensure_str(long_text.rstrip())
Josip Sokcevica4b871e2023-05-18 14:27:56 +0000332 if show_callstack is None:
333 show_callstack = _SHOW_CALLSTACKS
334 if show_callstack:
Bruce Dawsondca14bc2022-09-15 20:59:38 +0000335 self._long_text += 'Presubmit result call stack is:\n'
336 self._long_text += ''.join(traceback.format_stack(None, 8))
Tom McKee61c72652021-07-20 11:56:32 +0000337
338 @staticmethod
339 def _ensure_str(val):
340 """
341 val: A "stringish" value. Can be any of str, unicode or bytes.
342 returns: A str after applying encoding/decoding as needed.
343 Assumes/uses UTF-8 for relevant inputs/outputs.
344
345 We'd prefer to use six.ensure_str but our copy of six is old :(
346 """
347 if isinstance(val, str):
348 return val
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000349
Tom McKee61c72652021-07-20 11:56:32 +0000350 if six.PY2 and isinstance(val, unicode):
351 return val.encode()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000352
353 if six.PY3 and isinstance(val, bytes):
Tom McKee61c72652021-07-20 11:56:32 +0000354 return val.decode()
355 raise ValueError("Unknown string type %s" % type(val))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000356
Edward Lemur6eb1d322020-02-27 22:20:15 +0000357 def handle(self):
358 sys.stdout.write(self._message)
359 sys.stdout.write('\n')
Takuto Ikutabaa7be02022-08-23 00:19:34 +0000360 for item in self._items:
Edward Lemur6eb1d322020-02-27 22:20:15 +0000361 sys.stdout.write(' ')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000362 # Write separately in case it's unicode.
Edward Lemur6eb1d322020-02-27 22:20:15 +0000363 sys.stdout.write(str(item))
Edward Lemur6eb1d322020-02-27 22:20:15 +0000364 sys.stdout.write('\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000365 if self._long_text:
Edward Lemur6eb1d322020-02-27 22:20:15 +0000366 sys.stdout.write('\n***************\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000367 # Write separately in case it's unicode.
Tom McKee61c72652021-07-20 11:56:32 +0000368 sys.stdout.write(self._long_text)
Edward Lemur6eb1d322020-02-27 22:20:15 +0000369 sys.stdout.write('\n***************\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000370
Debrian Figueroadd2737e2019-06-21 23:50:13 +0000371 def json_format(self):
372 return {
373 'message': self._message,
Debrian Figueroa6095d402019-06-28 18:47:18 +0000374 'items': [str(item) for item in self._items],
Debrian Figueroadd2737e2019-06-21 23:50:13 +0000375 'long_text': self._long_text,
376 'fatal': self.fatal
377 }
378
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000379
380# Top level object so multiprocessing can pickle
381# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000382class _PresubmitError(_PresubmitResult):
383 """A hard presubmit error."""
384 fatal = True
385
386
387# Top level object so multiprocessing can pickle
388# Public access through OutputApi object.
389class _PresubmitPromptWarning(_PresubmitResult):
390 """An warning that prompts the user if they want to continue."""
391 should_prompt = True
392
393
394# Top level object so multiprocessing can pickle
395# Public access through OutputApi object.
396class _PresubmitNotifyResult(_PresubmitResult):
397 """Just print something to the screen -- but it's not even a warning."""
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000398
399
400# Top level object so multiprocessing can pickle
401# Public access through OutputApi object.
402class _MailTextResult(_PresubmitResult):
403 """A warning that should be included in the review request email."""
404 def __init__(self, *args, **kwargs):
405 super(_MailTextResult, self).__init__()
406 raise NotImplementedError()
407
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000408class GerritAccessor(object):
409 """Limited Gerrit functionality for canned presubmit checks to work.
410
411 To avoid excessive Gerrit calls, caches the results.
412 """
413
Edward Lesmeseb1bd622021-03-01 19:54:07 +0000414 def __init__(self, url=None, project=None, branch=None):
415 self.host = urlparse.urlparse(url).netloc if url else None
416 self.project = project
417 self.branch = branch
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000418 self.cache = {}
Edward Lesmes8170c292021-03-19 20:04:43 +0000419 self.code_owners_enabled = None
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000420
421 def _FetchChangeDetail(self, issue):
422 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100423 try:
424 return gerrit_util.GetChangeDetail(
425 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700426 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100427 except gerrit_util.GerritError as e:
428 if e.http_status == 404:
429 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
430 'no credentials to fetch issue details' % issue)
431 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000432
433 def GetChangeInfo(self, issue):
434 """Returns labels and all revisions (patchsets) for this issue.
435
436 The result is a dictionary according to Gerrit REST Api.
437 https://gerrit-review.googlesource.com/Documentation/rest-api.html
438
439 However, API isn't very clear what's inside, so see tests for example.
440 """
441 assert issue
442 cache_key = int(issue)
443 if cache_key not in self.cache:
444 self.cache[cache_key] = self._FetchChangeDetail(issue)
445 return self.cache[cache_key]
446
447 def GetChangeDescription(self, issue, patchset=None):
448 """If patchset is none, fetches current patchset."""
449 info = self.GetChangeInfo(issue)
450 # info is a reference to cache. We'll modify it here adding description to
451 # it to the right patchset, if it is not yet there.
452
453 # Find revision info for the patchset we want.
454 if patchset is not None:
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000455 for rev, rev_info in info['revisions'].items():
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000456 if str(rev_info['_number']) == str(patchset):
457 break
458 else:
459 raise Exception('patchset %s doesn\'t exist in issue %s' % (
460 patchset, issue))
461 else:
462 rev = info['current_revision']
463 rev_info = info['revisions'][rev]
464
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100465 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000466
Mun Yong Jang603d01e2017-12-19 16:38:30 -0800467 def GetDestRef(self, issue):
468 ref = self.GetChangeInfo(issue)['branch']
469 if not ref.startswith('refs/'):
470 # NOTE: it is possible to create 'refs/x' branch,
471 # aka 'refs/heads/refs/x'. However, this is ill-advised.
472 ref = 'refs/heads/%s' % ref
473 return ref
474
Edward Lesmes02d4b822020-11-11 00:37:35 +0000475 def _GetApproversForLabel(self, issue, label):
476 change_info = self.GetChangeInfo(issue)
477 label_info = change_info.get('labels', {}).get(label, {})
478 values = label_info.get('values', {}).keys()
479 if not values:
480 return []
481 max_value = max(int(v) for v in values)
482 return [v for v in label_info.get('all', [])
483 if v.get('value', 0) == max_value]
484
Edward Lesmesc4566172021-03-19 16:55:13 +0000485 def IsBotCommitApproved(self, issue):
486 return bool(self._GetApproversForLabel(issue, 'Bot-Commit'))
487
Edward Lesmescf49cb82020-11-11 01:08:36 +0000488 def IsOwnersOverrideApproved(self, issue):
489 return bool(self._GetApproversForLabel(issue, 'Owners-Override'))
490
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000491 def GetChangeOwner(self, issue):
492 return self.GetChangeInfo(issue)['owner']['email']
493
494 def GetChangeReviewers(self, issue, approving_only=True):
Aaron Gable8b478f02017-07-31 15:33:19 -0700495 changeinfo = self.GetChangeInfo(issue)
496 if approving_only:
Edward Lesmes02d4b822020-11-11 00:37:35 +0000497 reviewers = self._GetApproversForLabel(issue, 'Code-Review')
Aaron Gable8b478f02017-07-31 15:33:19 -0700498 else:
499 reviewers = changeinfo.get('reviewers', {}).get('REVIEWER', [])
500 return [r.get('email') for r in reviewers]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000501
Edward Lemure4d329c2020-02-03 20:41:18 +0000502 def UpdateDescription(self, description, issue):
503 gerrit_util.SetCommitMessage(self.host, issue, description, notify='NONE')
504
Edward Lesmes8170c292021-03-19 20:04:43 +0000505 def IsCodeOwnersEnabledOnRepo(self):
506 if self.code_owners_enabled is None:
507 self.code_owners_enabled = gerrit_util.IsCodeOwnersEnabledOnRepo(
Edward Lesmes392c4072021-03-19 21:58:45 +0000508 self.host, self.project)
Edward Lesmes8170c292021-03-19 20:04:43 +0000509 return self.code_owners_enabled
510
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000511
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000512class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000513 """An instance of OutputApi gets passed to presubmit scripts so that they
514 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000515 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000516 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000517 PresubmitError = _PresubmitError
518 PresubmitPromptWarning = _PresubmitPromptWarning
519 PresubmitNotifyResult = _PresubmitNotifyResult
520 MailTextResult = _MailTextResult
521
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000522 def __init__(self, is_committing):
523 self.is_committing = is_committing
Daniel Cheng7227d212017-11-17 08:12:37 -0800524 self.more_cc = []
525
526 def AppendCC(self, cc):
527 """Appends a user to cc for this change."""
Daniel Cheng541638f2023-05-15 22:00:47 +0000528 self.more_cc.append(cc)
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000529
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000530 def PresubmitPromptOrNotify(self, *args, **kwargs):
531 """Warn the user when uploading, but only notify if committing."""
532 if self.is_committing:
533 return self.PresubmitNotifyResult(*args, **kwargs)
534 return self.PresubmitPromptWarning(*args, **kwargs)
535
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000536
537class InputApi(object):
538 """An instance of this object is passed to presubmit scripts so they can
539 know stuff about the change they're looking at.
540 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000541 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800542 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000543
maruel@chromium.org3410d912009-06-09 20:56:16 +0000544 # File extensions that are considered source files from a style guide
545 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000546 #
547 # Files without an extension aren't included in the list. If you want to
local_bot30f774e2020-06-25 18:23:34 +0000548 # filter them as source files, add r'(^|.*?[\\\/])[^.]+$' to the allow list.
local_bot64021412020-07-08 21:05:39 +0000549 # Note that ALL CAPS files are skipped in DEFAULT_FILES_TO_SKIP below.
550 DEFAULT_FILES_TO_CHECK = (
maruel@chromium.org3410d912009-06-09 20:56:16 +0000551 # C++ and friends
Edward Lemura5799e32020-01-17 19:26:51 +0000552 r'.+\.c$', r'.+\.cc$', r'.+\.cpp$', r'.+\.h$', r'.+\.m$', r'.+\.mm$',
553 r'.+\.inl$', r'.+\.asm$', r'.+\.hxx$', r'.+\.hpp$', r'.+\.s$', r'.+\.S$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000554 # Scripts
dpapad443d9132022-05-05 00:17:30 +0000555 r'.+\.js$', r'.+\.ts$', r'.+\.py$', r'.+\.sh$', r'.+\.rb$', r'.+\.pl$',
556 r'.+\.pm$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000557 # Other
Edward Lemura5799e32020-01-17 19:26:51 +0000558 r'.+\.java$', r'.+\.mk$', r'.+\.am$', r'.+\.css$', r'.+\.mojom$',
Bruce Dawson7a81ebf2023-01-03 18:36:18 +0000559 r'.+\.fidl$', r'.+\.rs$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000560 )
561
562 # Path regexp that should be excluded from being considered containing source
563 # files. Don't modify this list from a presubmit script!
local_bot64021412020-07-08 21:05:39 +0000564 DEFAULT_FILES_TO_SKIP = (
Edward Lemura5799e32020-01-17 19:26:51 +0000565 r'testing_support[\\\/]google_appengine[\\\/].*',
566 r'.*\bexperimental[\\\/].*',
Kent Tamura179dd1e2018-04-26 15:07:41 +0900567 # Exclude third_party/.* but NOT third_party/{WebKit,blink}
568 # (crbug.com/539768 and crbug.com/836555).
Edward Lemura5799e32020-01-17 19:26:51 +0000569 r'.*\bthird_party[\\\/](?!(WebKit|blink)[\\\/]).*',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000570 # Output directories (just in case)
Edward Lemura5799e32020-01-17 19:26:51 +0000571 r'.*\bDebug[\\\/].*',
572 r'.*\bRelease[\\\/].*',
573 r'.*\bxcodebuild[\\\/].*',
574 r'.*\bout[\\\/].*',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000575 # All caps files like README and LICENCE.
Edward Lemura5799e32020-01-17 19:26:51 +0000576 r'.*\b[A-Z0-9_]{2,}$',
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000577 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
Edward Lemura5799e32020-01-17 19:26:51 +0000578 r'(|.*[\\\/])\.git[\\\/].*',
579 r'(|.*[\\\/])\.svn[\\\/].*',
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000580 # There is no point in processing a patch file.
Edward Lemura5799e32020-01-17 19:26:51 +0000581 r'.+\.diff$',
582 r'.+\.patch$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000583 )
584
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000585 def __init__(self, change, presubmit_path, is_committing,
Bruce Dawson09c0c072022-05-26 20:28:58 +0000586 verbose, gerrit_obj, dry_run=None, thread_pool=None, parallel=False,
587 no_diffs=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000588 """Builds an InputApi object.
589
590 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000591 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000592 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000593 is_committing: True if the change is about to be committed.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000594 gerrit_obj: provides basic Gerrit codereview functionality.
595 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -0400596 parallel: if true, all tests reported via input_api.RunTests for all
597 PRESUBMIT files will be run in parallel.
Bruce Dawson09c0c072022-05-26 20:28:58 +0000598 no_diffs: if true, implies that --files or --all was specified so some
599 checks can be skipped, and some errors will be messages.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000600 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000601 # Version number of the presubmit_support script.
602 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000603 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000604 self.is_committing = is_committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000605 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000606 self.dry_run = dry_run
Bruce Dawson09c0c072022-05-26 20:28:58 +0000607 self.no_diffs = no_diffs
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000608
Edward Lesmes8e282792018-04-03 18:50:29 -0400609 self.parallel = parallel
610 self.thread_pool = thread_pool or ThreadPool()
611
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000612 # We expose various modules and functions as attributes of the input_api
613 # so that presubmit scripts don't have to import them.
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +0900614 self.ast = ast
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000615 self.basename = os.path.basename
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000616 self.cpplint = cpplint
dcheng091b7db2016-06-16 01:27:51 -0700617 self.fnmatch = fnmatch
Yoshisato Yanagisawa04600b42019-03-15 03:03:41 +0000618 self.gclient_paths = gclient_paths
Yoshisato Yanagisawa57dd17b2019-03-22 09:10:29 +0000619 # TODO(yyanagisawa): stop exposing this when python3 become default.
620 # Since python3's tempfile has TemporaryDirectory, we do not need this.
621 self.temporary_directory = gclient_utils.temporary_directory
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000622 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000623 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000624 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000625 self.os_listdir = os.listdir
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000626 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000627 self.os_stat = os.stat
Yoshisato Yanagisawa406de132018-06-29 05:43:25 +0000628 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000629 self.re = re
630 self.subprocess = subprocess
Edward Lemurb9830242019-10-30 22:19:20 +0000631 self.sys = sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000632 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000633 self.time = time
maruel@chromium.org1487d532009-06-06 00:22:57 +0000634 self.unittest = unittest
Edward Lemurb9830242019-10-30 22:19:20 +0000635 if sys.version_info.major == 2:
636 self.urllib2 = urllib2
Edward Lemur16af3562019-10-17 22:11:33 +0000637 self.urllib_request = urllib_request
638 self.urllib_error = urllib_error
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000639
Robert Iannucci50258932018-03-19 10:30:59 -0700640 self.is_windows = sys.platform == 'win32'
641
Bruce Dawsonb6cb9e02023-05-18 20:52:43 +0000642 # Set python_executable to 'vpython3' in order to allow scripts in other
Edward Lemurb9646622019-10-25 20:57:35 +0000643 # repos (e.g. src.git) to automatically pick up that repo's .vpython file,
644 # instead of inheriting the one in depot_tools.
Bruce Dawsonb6cb9e02023-05-18 20:52:43 +0000645 self.python_executable = 'vpython3'
Erik Staab69135d12021-05-14 22:31:57 +0000646 # Offer a python 3 executable for use during the migration off of python 2.
647 self.python3_executable = 'vpython3'
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000648 self.environ = os.environ
649
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000650 # InputApi.platform is the platform you're currently running on.
651 self.platform = sys.platform
652
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000653 self.cpu_count = multiprocessing.cpu_count()
Bruce Dawson8254f062022-06-17 17:33:08 +0000654 if self.is_windows:
655 # TODO(crbug.com/1190269) - we can't use more than 56 child processes on
656 # Windows or Python3 may hang.
657 self.cpu_count = min(self.cpu_count, 56)
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000658
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000659 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000660 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000661
662 # We carry the canned checks so presubmit scripts can easily use them.
663 self.canned_checks = presubmit_canned_checks
664
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100665 # Temporary files we must manually remove at the end of a run.
666 self._named_temporary_files = []
Jochen Eisinger72606f82017-04-04 10:44:18 +0200667
Edward Lesmesdc11c6b2021-03-19 19:22:05 +0000668 self.owners_client = None
Bruce Dawsoneb8426e2022-08-05 23:58:15 +0000669 if self.gerrit and not 'PRESUBMIT_SKIP_NETWORK' in self.environ:
Bruce Dawson9b9f4512022-04-27 00:56:52 +0000670 try:
671 self.owners_client = owners_client.GetCodeOwnersClient(
Bruce Dawson9b9f4512022-04-27 00:56:52 +0000672 host=self.gerrit.host,
673 project=self.gerrit.project,
674 branch=self.gerrit.branch)
675 except Exception as e:
676 print('Failed to set owners_client - %s' % str(e))
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200677 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000678 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000679 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000680
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000681 # Replace <hash_map> and <hash_set> as headers that need to be included
Edward Lemura5799e32020-01-17 19:26:51 +0000682 # with 'base/containers/hash_tables.h' instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000683 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800684 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000685 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000686 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000687 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
688 for (a, b, header) in cpplint._re_pattern_templates
689 ]
690
Edward Lemurecc27072020-01-06 16:42:34 +0000691 def SetTimeout(self, timeout):
692 self.thread_pool.timeout = timeout
693
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000694 def PresubmitLocalPath(self):
695 """Returns the local path of the presubmit script currently being run.
696
697 This is useful if you don't want to hard-code absolute paths in the
698 presubmit script. For example, It can be used to find another file
699 relative to the PRESUBMIT.py script, so the whole tree can be branched and
700 the presubmit script still works, without editing its content.
701 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000702 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000703
agable0b65e732016-11-22 09:25:46 -0800704 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000705 """Same as input_api.change.AffectedFiles() except only lists files
706 (and optionally directories) in the same directory as the current presubmit
Bruce Dawson7a0b07a2020-04-23 17:14:40 +0000707 script, or subdirectories thereof. Note that files are listed using the OS
708 path separator, so backslashes are used as separators on Windows.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709 """
Josip Sokcevica9a7eec2023-03-10 03:54:52 +0000710 dir_with_slash = normpath(self.PresubmitLocalPath())
Bruce Dawson31bfd512022-05-10 23:19:39 +0000711 # normpath strips trailing path separators, so the trailing separator has to
712 # be added after the normpath call.
713 if len(dir_with_slash) > 0:
714 dir_with_slash += os.path.sep
sail@chromium.org5538e022011-05-12 17:53:16 +0000715
Josip Sokcevica9a7eec2023-03-10 03:54:52 +0000716 return list(filter(
717 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
718 self.change.AffectedFiles(include_deletes, file_filter)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000719
agable0b65e732016-11-22 09:25:46 -0800720 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000721 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800722 paths = [af.LocalPath() for af in self.AffectedFiles()]
Edward Lemura5799e32020-01-17 19:26:51 +0000723 logging.debug('LocalPaths: %s', paths)
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000724 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000725
agable0b65e732016-11-22 09:25:46 -0800726 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000727 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800728 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000729
John Budorick16162372018-04-18 10:39:53 -0700730 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
agable0b65e732016-11-22 09:25:46 -0800731 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000732 in the same directory as the current presubmit script, or subdirectories
733 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000734 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000735 if include_deletes is not None:
Edward Lemura5799e32020-01-17 19:26:51 +0000736 warn('AffectedTestableFiles(include_deletes=%s)'
737 ' is deprecated and ignored' % str(include_deletes),
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000738 category=DeprecationWarning,
739 stacklevel=2)
Josip Sokcevic4de5dea2022-03-23 21:15:14 +0000740 # pylint: disable=consider-using-generator
741 return [
742 x for x in self.AffectedFiles(include_deletes=False, **kwargs)
743 if x.IsTestableFile()
744 ]
agable0b65e732016-11-22 09:25:46 -0800745
746 def AffectedTextFiles(self, include_deletes=None):
747 """An alias to AffectedTestableFiles for backwards compatibility."""
748 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000749
Josip Sokcevic8c955952021-02-01 21:32:57 +0000750 def FilterSourceFile(self,
751 affected_file,
752 files_to_check=None,
753 files_to_skip=None,
754 allow_list=None,
755 block_list=None):
Edward Lemura5799e32020-01-17 19:26:51 +0000756 """Filters out files that aren't considered 'source file'.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000757
local_bot64021412020-07-08 21:05:39 +0000758 If files_to_check or files_to_skip is None, InputApi.DEFAULT_FILES_TO_CHECK
759 and InputApi.DEFAULT_FILES_TO_SKIP is used respectively.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000760
Bruce Dawson635383f2022-09-13 16:23:18 +0000761 affected_file.LocalPath() needs to re.match an entry in the files_to_check
762 list and not re.match any entries in the files_to_skip list.
763 '/' path separators should be used in the regular expressions and will work
764 on Windows as well as other platforms.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000765
766 Note: Copy-paste this function to suit your needs or use a lambda function.
767 """
local_bot64021412020-07-08 21:05:39 +0000768 if files_to_check is None:
769 files_to_check = self.DEFAULT_FILES_TO_CHECK
770 if files_to_skip is None:
771 files_to_skip = self.DEFAULT_FILES_TO_SKIP
local_bot30f774e2020-06-25 18:23:34 +0000772
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000773 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000774 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000775 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000776 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000777 return True
Bruce Dawsona3a014e2022-04-27 23:28:17 +0000778 # Handle the cases where the files regex only handles /, but the local
779 # path uses \.
780 if self.is_windows and self.re.match(item, local_path.replace(
781 '\\', '/')):
782 return True
maruel@chromium.org3410d912009-06-09 20:56:16 +0000783 return False
local_bot64021412020-07-08 21:05:39 +0000784 return (Find(affected_file, files_to_check) and
785 not Find(affected_file, files_to_skip))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000786
787 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800788 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000789
790 If source_file is None, InputApi.FilterSourceFile() is used.
791 """
792 if not source_file:
793 source_file = self.FilterSourceFile
Edward Lemurb9830242019-10-30 22:19:20 +0000794 return list(filter(source_file, self.AffectedTestableFiles()))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000795
796 def RightHandSideLines(self, source_file_filter=None):
Edward Lemura5799e32020-01-17 19:26:51 +0000797 """An iterator over all text lines in 'new' version of changed files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000798
799 Only lists lines from new or modified text files in the change that are
800 contained by the directory of the currently executing presubmit script.
801
802 This is useful for doing line-by-line regex checks, like checking for
803 trailing whitespace.
804
805 Yields:
806 a 3 tuple:
Josip Sokcevic7958e302023-03-01 23:02:21 +0000807 the AffectedFile instance of the current file;
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000808 integer line number (1-based); and
809 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000810
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000811 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000812 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000813 files = self.AffectedSourceFiles(source_file_filter)
Josip Sokcevic7958e302023-03-01 23:02:21 +0000814 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000815
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000816 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000817 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000818
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000819 Deny reading anything outside the repository.
820 """
Josip Sokcevic7958e302023-03-01 23:02:21 +0000821 if isinstance(file_item, AffectedFile):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000822 file_item = file_item.AbsoluteLocalPath()
823 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000824 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000825 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000826
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100827 def CreateTemporaryFile(self, **kwargs):
828 """Returns a named temporary file that must be removed with a call to
829 RemoveTemporaryFiles().
830
831 All keyword arguments are forwarded to tempfile.NamedTemporaryFile(),
832 except for |delete|, which is always set to False.
833
834 Presubmit checks that need to create a temporary file and pass it for
835 reading should use this function instead of NamedTemporaryFile(), as
836 Windows fails to open a file that is already open for writing.
837
838 with input_api.CreateTemporaryFile() as f:
839 f.write('xyz')
840 f.close()
841 input_api.subprocess.check_output(['script-that', '--reads-from',
842 f.name])
843
844
845 Note that callers of CreateTemporaryFile() should not worry about removing
846 any temporary file; this is done transparently by the presubmit handling
847 code.
848 """
849 if 'delete' in kwargs:
850 # Prevent users from passing |delete|; we take care of file deletion
851 # ourselves and this prevents unintuitive error messages when we pass
852 # delete=False and 'delete' is also in kwargs.
853 raise TypeError('CreateTemporaryFile() does not take a "delete" '
854 'argument, file deletion is handled automatically by '
855 'the same presubmit_support code that creates InputApi '
856 'objects.')
857 temp_file = self.tempfile.NamedTemporaryFile(delete=False, **kwargs)
858 self._named_temporary_files.append(temp_file.name)
859 return temp_file
860
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000861 @property
862 def tbr(self):
863 """Returns if a change is TBR'ed."""
Jeremy Romandce22502017-06-20 15:37:29 -0400864 return 'TBR' in self.change.tags or self.change.TBRsFromDescription()
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000865
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000866 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000867 tests = []
868 msgs = []
869 for t in tests_mix:
Edward Lesmes8e282792018-04-03 18:50:29 -0400870 if isinstance(t, OutputApi.PresubmitResult) and t:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000871 msgs.append(t)
872 else:
873 assert issubclass(t.message, _PresubmitResult)
874 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000875 if self.verbose:
876 t.info = _PresubmitNotifyResult
Edward Lemur1037c742018-05-01 18:56:04 -0400877 if not t.kwargs.get('cwd'):
878 t.kwargs['cwd'] = self.PresubmitLocalPath()
Edward Lesmes8e282792018-04-03 18:50:29 -0400879 self.thread_pool.AddTests(tests, parallel)
Edward Lemur21000eb2019-05-24 23:25:58 +0000880 # When self.parallel is True (i.e. --parallel is passed as an option)
881 # RunTests doesn't actually run tests. It adds them to a ThreadPool that
882 # will run all tests once all PRESUBMIT files are processed.
883 # Otherwise, it will run them and return the results.
884 if not self.parallel:
Edward Lesmes8e282792018-04-03 18:50:29 -0400885 msgs.extend(self.thread_pool.RunAsync())
886 return msgs
scottmg86099d72016-09-01 09:16:51 -0700887
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000888
Josip Sokcevic7958e302023-03-01 23:02:21 +0000889class _DiffCache(object):
890 """Caches diffs retrieved from a particular SCM."""
891 def __init__(self, upstream=None):
892 """Stores the upstream revision against which all diffs will be computed."""
893 self._upstream = upstream
894
895 def GetDiff(self, path, local_root):
896 """Get the diff for a particular path."""
897 raise NotImplementedError()
898
899 def GetOldContents(self, path, local_root):
900 """Get the old version for a particular path."""
901 raise NotImplementedError()
902
903
904class _GitDiffCache(_DiffCache):
905 """DiffCache implementation for git; gets all file diffs at once."""
906 def __init__(self, upstream):
907 super(_GitDiffCache, self).__init__(upstream=upstream)
908 self._diffs_by_file = None
909
910 def GetDiff(self, path, local_root):
911 # Compare against None to distinguish between None and an initialized but
912 # empty dictionary.
913 if self._diffs_by_file == None:
914 # Compute a single diff for all files and parse the output; should
915 # with git this is much faster than computing one diff for each file.
916 diffs = {}
917
918 # Don't specify any filenames below, because there are command line length
919 # limits on some platforms and GenerateDiff would fail.
920 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
921 branch=self._upstream)
922
923 # This regex matches the path twice, separated by a space. Note that
924 # filename itself may contain spaces.
925 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
926 current_diff = []
927 keep_line_endings = True
928 for x in unified_diff.splitlines(keep_line_endings):
929 match = file_marker.match(x)
930 if match:
931 # Marks the start of a new per-file section.
932 diffs[match.group('filename')] = current_diff = [x]
933 elif x.startswith('diff --git'):
934 raise PresubmitFailure('Unexpected diff line: %s' % x)
935 else:
936 current_diff.append(x)
937
938 self._diffs_by_file = dict(
Josip Sokcevica9a7eec2023-03-10 03:54:52 +0000939 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
Josip Sokcevic7958e302023-03-01 23:02:21 +0000940
941 if path not in self._diffs_by_file:
942 # SCM didn't have any diff on this file. It could be that the file was not
943 # modified at all (e.g. user used --all flag in git cl presubmit).
944 # Intead of failing, return empty string.
945 # See: https://crbug.com/808346.
946 return ''
947
948 return self._diffs_by_file[path]
949
950 def GetOldContents(self, path, local_root):
951 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
952
953
954class AffectedFile(object):
955 """Representation of a file in a change."""
956
957 DIFF_CACHE = _DiffCache
958
959 # Method could be a function
960 # pylint: disable=no-self-use
961 def __init__(self, path, action, repository_root, diff_cache):
962 self._path = path
963 self._action = action
964 self._local_root = repository_root
965 self._is_directory = None
966 self._cached_changed_contents = None
967 self._cached_new_contents = None
968 self._diff_cache = diff_cache
969 logging.debug('%s(%s)', self.__class__.__name__, self._path)
970
971 def LocalPath(self):
972 """Returns the path of this file on the local disk relative to client root.
973
974 This should be used for error messages but not for accessing files,
975 because presubmit checks are run with CWD=PresubmitLocalPath() (which is
976 often != client root).
977 """
Josip Sokcevica9a7eec2023-03-10 03:54:52 +0000978 return normpath(self._path)
Josip Sokcevic7958e302023-03-01 23:02:21 +0000979
980 def AbsoluteLocalPath(self):
981 """Returns the absolute path of this file on the local disk.
982 """
983 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
984
985 def Action(self):
986 """Returns the action on this opened file, e.g. A, M, D, etc."""
987 return self._action
988
989 def IsTestableFile(self):
990 """Returns True if the file is a text file and not a binary file.
991
992 Deleted files are not text file."""
993 raise NotImplementedError() # Implement when needed
994
995 def IsTextFile(self):
996 """An alias to IsTestableFile for backwards compatibility."""
997 return self.IsTestableFile()
998
999 def OldContents(self):
1000 """Returns an iterator over the lines in the old version of file.
1001
1002 The old version is the file before any modifications in the user's
1003 workspace, i.e. the 'left hand side'.
1004
1005 Contents will be empty if the file is a directory or does not exist.
1006 Note: The carriage returns (LF or CR) are stripped off.
1007 """
1008 return self._diff_cache.GetOldContents(self.LocalPath(),
1009 self._local_root).splitlines()
1010
1011 def NewContents(self):
1012 """Returns an iterator over the lines in the new version of file.
1013
1014 The new version is the file in the user's workspace, i.e. the 'right hand
1015 side'.
1016
1017 Contents will be empty if the file is a directory or does not exist.
1018 Note: The carriage returns (LF or CR) are stripped off.
1019 """
1020 if self._cached_new_contents is None:
1021 self._cached_new_contents = []
1022 try:
1023 self._cached_new_contents = gclient_utils.FileRead(
1024 self.AbsoluteLocalPath(), 'rU').splitlines()
1025 except IOError:
1026 pass # File not found? That's fine; maybe it was deleted.
1027 except UnicodeDecodeError as e:
1028 # log the filename since we're probably trying to read a binary
1029 # file, and shouldn't be.
1030 print('Error reading %s: %s' % (self.AbsoluteLocalPath(), e))
1031 raise
1032
1033 return self._cached_new_contents[:]
1034
1035 def ChangedContents(self, keeplinebreaks=False):
1036 """Returns a list of tuples (line number, line text) of all new lines.
1037
1038 This relies on the scm diff output describing each changed code section
1039 with a line of the form
1040
1041 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
1042 """
1043 # Don't return cached results when line breaks are requested.
1044 if not keeplinebreaks and self._cached_changed_contents is not None:
1045 return self._cached_changed_contents[:]
1046 result = []
1047 line_num = 0
1048
1049 # The keeplinebreaks parameter to splitlines must be True or else the
1050 # CheckForWindowsLineEndings presubmit will be a NOP.
1051 for line in self.GenerateScmDiff().splitlines(keeplinebreaks):
1052 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
1053 if m:
1054 line_num = int(m.groups(1)[0])
1055 continue
1056 if line.startswith('+') and not line.startswith('++'):
1057 result.append((line_num, line[1:]))
1058 if not line.startswith('-'):
1059 line_num += 1
1060 # Don't cache results with line breaks.
1061 if keeplinebreaks:
1062 return result;
1063 self._cached_changed_contents = result
1064 return self._cached_changed_contents[:]
1065
1066 def __str__(self):
1067 return self.LocalPath()
1068
1069 def GenerateScmDiff(self):
1070 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
1071
1072
1073class GitAffectedFile(AffectedFile):
1074 """Representation of a file in a change out of a git checkout."""
1075 # Method 'NNN' is abstract in class 'NNN' but is not overridden
1076 # pylint: disable=abstract-method
1077
1078 DIFF_CACHE = _GitDiffCache
1079
1080 def __init__(self, *args, **kwargs):
1081 AffectedFile.__init__(self, *args, **kwargs)
1082 self._server_path = None
1083 self._is_testable_file = None
1084
1085 def IsTestableFile(self):
1086 if self._is_testable_file is None:
1087 if self.Action() == 'D':
1088 # A deleted file is not testable.
1089 self._is_testable_file = False
1090 else:
1091 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
1092 return self._is_testable_file
1093
1094
1095class Change(object):
1096 """Describe a change.
1097
1098 Used directly by the presubmit scripts to query the current change being
1099 tested.
1100
1101 Instance members:
1102 tags: Dictionary of KEY=VALUE pairs found in the change description.
1103 self.KEY: equivalent to tags['KEY']
1104 """
1105
1106 _AFFECTED_FILES = AffectedFile
1107
1108 # Matches key/value (or 'tag') lines in changelist descriptions.
1109 TAG_LINE_RE = re.compile(
1110 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
1111 scm = ''
1112
1113 def __init__(
1114 self, name, description, local_root, files, issue, patchset, author,
1115 upstream=None):
1116 if files is None:
1117 files = []
1118 self._name = name
1119 # Convert root into an absolute path.
1120 self._local_root = os.path.abspath(local_root)
1121 self._upstream = upstream
1122 self.issue = issue
1123 self.patchset = patchset
1124 self.author_email = author
1125
1126 self._full_description = ''
1127 self.tags = {}
1128 self._description_without_tags = ''
1129 self.SetDescriptionText(description)
1130
1131 assert all(
1132 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
1133
1134 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
1135 self._affected_files = [
1136 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
1137 for action, path in files
1138 ]
1139
1140 def UpstreamBranch(self):
1141 """Returns the upstream branch for the change."""
1142 return self._upstream
1143
1144 def Name(self):
1145 """Returns the change name."""
1146 return self._name
1147
1148 def DescriptionText(self):
1149 """Returns the user-entered changelist description, minus tags.
1150
1151 Any line in the user-provided description starting with e.g. 'FOO='
1152 (whitespace permitted before and around) is considered a tag line. Such
1153 lines are stripped out of the description this function returns.
1154 """
1155 return self._description_without_tags
1156
1157 def FullDescriptionText(self):
1158 """Returns the complete changelist description including tags."""
1159 return self._full_description
1160
1161 def SetDescriptionText(self, description):
1162 """Sets the full description text (including tags) to |description|.
1163
1164 Also updates the list of tags."""
1165 self._full_description = description
1166
1167 # From the description text, build up a dictionary of key/value pairs
1168 # plus the description minus all key/value or 'tag' lines.
1169 description_without_tags = []
1170 self.tags = {}
1171 for line in self._full_description.splitlines():
1172 m = self.TAG_LINE_RE.match(line)
1173 if m:
1174 self.tags[m.group('key')] = m.group('value')
1175 else:
1176 description_without_tags.append(line)
1177
1178 # Change back to text and remove whitespace at end.
1179 self._description_without_tags = (
1180 '\n'.join(description_without_tags).rstrip())
1181
1182 def AddDescriptionFooter(self, key, value):
1183 """Adds the given footer to the change description.
1184
1185 Args:
1186 key: A string with the key for the git footer. It must conform to
1187 the git footers format (i.e. 'List-Of-Tokens') and will be case
1188 normalized so that each token is title-cased.
1189 value: A string with the value for the git footer.
1190 """
1191 description = git_footers.add_footer(
1192 self.FullDescriptionText(), git_footers.normalize_name(key), value)
1193 self.SetDescriptionText(description)
1194
1195 def RepositoryRoot(self):
1196 """Returns the repository (checkout) root directory for this change,
1197 as an absolute path.
1198 """
1199 return self._local_root
1200
1201 def __getattr__(self, attr):
1202 """Return tags directly as attributes on the object."""
1203 if not re.match(r'^[A-Z_]*$', attr):
1204 raise AttributeError(self, attr)
1205 return self.tags.get(attr)
1206
1207 def GitFootersFromDescription(self):
1208 """Return the git footers present in the description.
1209
1210 Returns:
1211 footers: A dict of {footer: [values]} containing a multimap of the footers
1212 in the change description.
1213 """
1214 return git_footers.parse_footers(self.FullDescriptionText())
1215
1216 def BugsFromDescription(self):
1217 """Returns all bugs referenced in the commit description."""
1218 bug_tags = ['BUG', 'FIXED']
1219
1220 tags = []
1221 for tag in bug_tags:
1222 values = self.tags.get(tag)
1223 if values:
1224 tags += [value.strip() for value in values.split(',')]
1225
1226 footers = []
1227 parsed = self.GitFootersFromDescription()
1228 unsplit_footers = parsed.get('Bug', []) + parsed.get('Fixed', [])
1229 for unsplit_footer in unsplit_footers:
1230 footers += [b.strip() for b in unsplit_footer.split(',')]
1231 return sorted(set(tags + footers))
1232
1233 def ReviewersFromDescription(self):
1234 """Returns all reviewers listed in the commit description."""
1235 # We don't support a 'R:' git-footer for reviewers; that is in metadata.
1236 tags = [r.strip() for r in self.tags.get('R', '').split(',') if r.strip()]
1237 return sorted(set(tags))
1238
1239 def TBRsFromDescription(self):
1240 """Returns all TBR reviewers listed in the commit description."""
1241 tags = [r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()]
1242 # TODO(crbug.com/839208): Remove support for 'Tbr:' when TBRs are
1243 # programmatically determined by self-CR+1s.
1244 footers = self.GitFootersFromDescription().get('Tbr', [])
1245 return sorted(set(tags + footers))
1246
1247 # TODO(crbug.com/753425): Delete these once we're sure they're unused.
1248 @property
1249 def BUG(self):
1250 return ','.join(self.BugsFromDescription())
1251 @property
1252 def R(self):
1253 return ','.join(self.ReviewersFromDescription())
1254 @property
1255 def TBR(self):
1256 return ','.join(self.TBRsFromDescription())
1257
1258 def AllFiles(self, root=None):
1259 """List all files under source control in the repo."""
1260 raise NotImplementedError()
1261
1262 def AffectedFiles(self, include_deletes=True, file_filter=None):
1263 """Returns a list of AffectedFile instances for all files in the change.
1264
1265 Args:
1266 include_deletes: If false, deleted files will be filtered out.
1267 file_filter: An additional filter to apply.
1268
1269 Returns:
1270 [AffectedFile(path, action), AffectedFile(path, action)]
1271 """
1272 affected = list(filter(file_filter, self._affected_files))
1273
1274 if include_deletes:
1275 return affected
1276 return list(filter(lambda x: x.Action() != 'D', affected))
1277
1278 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
1279 """Return a list of the existing text files in a change."""
1280 if include_deletes is not None:
1281 warn('AffectedTeestableFiles(include_deletes=%s)'
1282 ' is deprecated and ignored' % str(include_deletes),
1283 category=DeprecationWarning,
1284 stacklevel=2)
1285 return list(filter(
1286 lambda x: x.IsTestableFile(),
1287 self.AffectedFiles(include_deletes=False, **kwargs)))
1288
1289 def AffectedTextFiles(self, include_deletes=None):
1290 """An alias to AffectedTestableFiles for backwards compatibility."""
1291 return self.AffectedTestableFiles(include_deletes=include_deletes)
1292
1293 def LocalPaths(self):
1294 """Convenience function."""
1295 return [af.LocalPath() for af in self.AffectedFiles()]
1296
1297 def AbsoluteLocalPaths(self):
1298 """Convenience function."""
1299 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
1300
1301 def RightHandSideLines(self):
1302 """An iterator over all text lines in 'new' version of changed files.
1303
1304 Lists lines from new or modified text files in the change.
1305
1306 This is useful for doing line-by-line regex checks, like checking for
1307 trailing whitespace.
1308
1309 Yields:
1310 a 3 tuple:
1311 the AffectedFile instance of the current file;
1312 integer line number (1-based); and
1313 the contents of the line as a string.
1314 """
1315 return _RightHandSideLinesImpl(
1316 x for x in self.AffectedFiles(include_deletes=False)
1317 if x.IsTestableFile())
1318
1319 def OriginalOwnersFiles(self):
1320 """A map from path names of affected OWNERS files to their old content."""
1321 def owners_file_filter(f):
1322 return 'OWNERS' in os.path.split(f.LocalPath())[1]
1323 files = self.AffectedFiles(file_filter=owners_file_filter)
1324 return {f.LocalPath(): f.OldContents() for f in files}
1325
1326
1327class GitChange(Change):
1328 _AFFECTED_FILES = GitAffectedFile
1329 scm = 'git'
1330
1331 def AllFiles(self, root=None):
1332 """List all files under source control in the repo."""
1333 root = root or self.RepositoryRoot()
1334 return subprocess.check_output(
1335 ['git', '-c', 'core.quotePath=false', 'ls-files', '--', '.'],
1336 cwd=root).decode('utf-8', 'ignore').splitlines()
1337
1338
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001339def ListRelevantPresubmitFiles(files, root):
1340 """Finds all presubmit files that apply to a given set of source files.
1341
1342 If inherit-review-settings-ok is present right under root, looks for
1343 PRESUBMIT.py in directories enclosing root.
1344
1345 Args:
1346 files: An iterable container containing file paths.
1347 root: Path where to stop searching.
1348
1349 Return:
1350 List of absolute paths of the existing PRESUBMIT.py scripts.
1351 """
1352 files = [normpath(os.path.join(root, f)) for f in files]
1353
1354 # List all the individual directories containing files.
1355 directories = {os.path.dirname(f) for f in files}
1356
1357 # Ignore root if inherit-review-settings-ok is present.
1358 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1359 root = None
1360
1361 # Collect all unique directories that may contain PRESUBMIT.py.
1362 candidates = set()
1363 for directory in directories:
1364 while True:
1365 if directory in candidates:
1366 break
1367 candidates.add(directory)
1368 if directory == root:
1369 break
1370 parent_dir = os.path.dirname(directory)
1371 if parent_dir == directory:
1372 # We hit the system root directory.
1373 break
1374 directory = parent_dir
1375
1376 # Look for PRESUBMIT.py in all candidate directories.
1377 results = []
1378 for directory in sorted(list(candidates)):
1379 try:
1380 for f in os.listdir(directory):
1381 p = os.path.join(directory, f)
1382 if os.path.isfile(p) and re.match(
1383 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1384 results.append(p)
1385 except OSError:
1386 pass
1387
1388 logging.debug('Presubmit files: %s', ','.join(results))
1389 return results
1390
1391
rmistry@google.com5626a922015-02-26 14:03:30 +00001392class GetPostUploadExecuter(object):
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00001393 def __init__(self, change, gerrit_obj):
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001394 """
1395 Args:
Pavol Marko624e7ee2023-01-09 09:56:29 +00001396 change: The Change object.
1397 gerrit_obj: provides basic Gerrit codereview functionality.
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001398 """
Pavol Marko624e7ee2023-01-09 09:56:29 +00001399 self.change = change
1400 self.gerrit = gerrit_obj
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001401
Pavol Marko624e7ee2023-01-09 09:56:29 +00001402 def ExecPresubmitScript(self, script_text, presubmit_path):
rmistry@google.com5626a922015-02-26 14:03:30 +00001403 """Executes PostUploadHook() from a single presubmit script.
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001404 Caller is responsible for validating whether the hook should be executed
1405 and should only call this function if it should be.
rmistry@google.com5626a922015-02-26 14:03:30 +00001406
1407 Args:
1408 script_text: The text of the presubmit script.
1409 presubmit_path: Project script to run.
rmistry@google.com5626a922015-02-26 14:03:30 +00001410
1411 Return:
1412 A list of results objects.
1413 """
Pavol Marko624e7ee2023-01-09 09:56:29 +00001414 # Change to the presubmit file's directory to support local imports.
1415 presubmit_dir = os.path.dirname(presubmit_path)
1416 main_path = os.getcwd()
1417 try:
1418 os.chdir(presubmit_dir)
1419 return self._execute_with_local_working_directory(script_text,
1420 presubmit_dir,
1421 presubmit_path)
1422 finally:
1423 # Return the process to the original working directory.
1424 os.chdir(main_path)
1425
1426 def _execute_with_local_working_directory(self, script_text, presubmit_dir,
1427 presubmit_path):
rmistry@google.com5626a922015-02-26 14:03:30 +00001428 context = {}
1429 try:
Pavol Marko624e7ee2023-01-09 09:56:29 +00001430 exec(compile(script_text, presubmit_path, 'exec', dont_inherit=True),
Raul Tambre09e64b42019-05-14 01:57:22 +00001431 context)
Raul Tambre7c938462019-05-24 16:35:35 +00001432 except Exception as e:
rmistry@google.com5626a922015-02-26 14:03:30 +00001433 raise PresubmitFailure('"%s" had an exception.\n%s'
1434 % (presubmit_path, e))
1435
1436 function_name = 'PostUploadHook'
1437 if function_name not in context:
1438 return {}
1439 post_upload_hook = context[function_name]
1440 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1441 raise PresubmitFailure(
1442 'Expected function "PostUploadHook" to take three arguments.')
Pavol Marko624e7ee2023-01-09 09:56:29 +00001443 return post_upload_hook(self.gerrit, self.change, OutputApi(False))
rmistry@google.com5626a922015-02-26 14:03:30 +00001444
1445
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001446def _MergeMasters(masters1, masters2):
1447 """Merges two master maps. Merges also the tests of each builder."""
1448 result = {}
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001449 for (master, builders) in itertools.chain(masters1.items(),
1450 masters2.items()):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001451 new_builders = result.setdefault(master, {})
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001452 for (builder, tests) in builders.items():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001453 new_builders.setdefault(builder, set([])).update(tests)
1454 return result
1455
1456
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00001457def DoPostUploadExecuter(change, gerrit_obj, verbose):
rmistry@google.com5626a922015-02-26 14:03:30 +00001458 """Execute the post upload hook.
1459
1460 Args:
1461 change: The Change object.
Edward Lemur016a0872020-02-04 22:13:28 +00001462 gerrit_obj: The GerritAccessor object.
rmistry@google.com5626a922015-02-26 14:03:30 +00001463 verbose: Prints debug info.
rmistry@google.com5626a922015-02-26 14:03:30 +00001464 """
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001465 python_version = 'Python %s' % sys.version_info.major
1466 sys.stdout.write('Running %s post upload checks ...\n' % python_version)
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001467 presubmit_files = ListRelevantPresubmitFiles(
1468 change.LocalPaths(), change.RepositoryRoot())
rmistry@google.com5626a922015-02-26 14:03:30 +00001469 if not presubmit_files and verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001470 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
rmistry@google.com5626a922015-02-26 14:03:30 +00001471 results = []
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00001472 executer = GetPostUploadExecuter(change, gerrit_obj)
rmistry@google.com5626a922015-02-26 14:03:30 +00001473 # The root presubmit file should be executed after the ones in subdirectories.
1474 # i.e. the specific post upload hooks should run before the general ones.
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001475 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
rmistry@google.com5626a922015-02-26 14:03:30 +00001476 presubmit_files.reverse()
1477
1478 for filename in presubmit_files:
1479 filename = os.path.abspath(filename)
rmistry@google.com5626a922015-02-26 14:03:30 +00001480 # Accept CRLF presubmit script.
Bruce Dawson6e70e862022-07-01 19:37:01 +00001481 presubmit_script = gclient_utils.FileRead(filename).replace('\r\n', '\n')
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00001482 if verbose:
1483 sys.stdout.write('Running %s\n' % filename)
1484 results.extend(executer.ExecPresubmitScript(presubmit_script, filename))
rmistry@google.com5626a922015-02-26 14:03:30 +00001485
Edward Lemur6eb1d322020-02-27 22:20:15 +00001486 if not results:
1487 return 0
1488
1489 sys.stdout.write('\n')
1490 sys.stdout.write('** Post Upload Hook Messages **\n')
1491
1492 exit_code = 0
1493 for result in results:
1494 if result.fatal:
1495 exit_code = 1
1496 result.handle()
1497 sys.stdout.write('\n')
1498
1499 return exit_code
rmistry@google.com5626a922015-02-26 14:03:30 +00001500
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001501class PresubmitExecuter(object):
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001502 def __init__(self, change, committing, verbose, gerrit_obj, dry_run=None,
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00001503 thread_pool=None, parallel=False, no_diffs=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001504 """
1505 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001506 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001507 committing: True if 'git cl land' is running, False if 'git cl upload' is.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001508 gerrit_obj: provides basic Gerrit codereview functionality.
1509 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001510 parallel: if true, all tests reported via input_api.RunTests for all
1511 PRESUBMIT files will be run in parallel.
Bruce Dawson09c0c072022-05-26 20:28:58 +00001512 no_diffs: if true, implies that --files or --all was specified so some
1513 checks can be skipped, and some errors will be messages.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001514 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001515 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001516 self.committing = committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001517 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001518 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001519 self.dry_run = dry_run
Daniel Cheng7227d212017-11-17 08:12:37 -08001520 self.more_cc = []
Edward Lesmes8e282792018-04-03 18:50:29 -04001521 self.thread_pool = thread_pool
1522 self.parallel = parallel
Bruce Dawson09c0c072022-05-26 20:28:58 +00001523 self.no_diffs = no_diffs
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001524
1525 def ExecPresubmitScript(self, script_text, presubmit_path):
1526 """Executes a single presubmit script.
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001527 Caller is responsible for validating whether the hook should be executed
1528 and should only call this function if it should be.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001529
1530 Args:
1531 script_text: The text of the presubmit script.
1532 presubmit_path: The path to the presubmit file (this will be reported via
1533 input_api.PresubmitLocalPath()).
1534
1535 Return:
1536 A list of result objects, empty if no problems.
1537 """
chase@chromium.org8e416c82009-10-06 04:30:44 +00001538 # Change to the presubmit file's directory to support local imports.
Saagar Sanghavi98b332f2020-07-31 17:19:15 +00001539 presubmit_dir = os.path.dirname(presubmit_path)
Pavol Marko624e7ee2023-01-09 09:56:29 +00001540 main_path = os.getcwd()
1541 try:
1542 os.chdir(presubmit_dir)
1543 return self._execute_with_local_working_directory(script_text,
1544 presubmit_dir,
1545 presubmit_path)
1546 finally:
1547 # Return the process to the original working directory.
1548 os.chdir(main_path)
chase@chromium.org8e416c82009-10-06 04:30:44 +00001549
Pavol Marko624e7ee2023-01-09 09:56:29 +00001550 def _execute_with_local_working_directory(self, script_text, presubmit_dir,
1551 presubmit_path):
chase@chromium.org8e416c82009-10-06 04:30:44 +00001552 # 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:
Bruce Dawson0ba2fd42022-07-21 13:47:21 +00001561 exec(compile(script_text, presubmit_path, 'exec', dont_inherit=True),
Raul Tambre09e64b42019-05-14 01:57:22 +00001562 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)
Daniel Cheng541638f2023-05-15 22:00:47 +00001609 # Clear the CC list between running each presubmit check to prevent
1610 # CCs from being repeatedly appended.
1611 output_api.more_cc = []
Scott Leecc2fe9b2020-11-19 19:38:06 +00001612
1613 else: # Old format
1614 if self.committing:
1615 function_name = 'CheckChangeOnCommit'
1616 else:
1617 function_name = 'CheckChangeOnUpload'
Andrew Grievebdfb8f22022-02-02 21:56:47 +00001618 if function_name in list(context.keys()):
Scott Leecc2fe9b2020-11-19 19:38:06 +00001619 logging.debug('Running %s in %s', function_name, presubmit_path)
1620 results.extend(
Bruce Dawson8fa42e22022-03-29 17:05:38 +00001621 self._run_check_function(function_name, context, sink,
1622 presubmit_path))
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001623 logging.debug('Running %s done.', function_name)
1624 self.more_cc.extend(output_api.more_cc)
Daniel Cheng541638f2023-05-15 22:00:47 +00001625 # Clear the CC list between running each presubmit check to prevent
1626 # CCs from being repeatedly appended.
1627 output_api.more_cc = []
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001628
1629 finally:
1630 for f in input_api._named_temporary_files:
1631 os.remove(f)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001632
Daniel Cheng541638f2023-05-15 22:00:47 +00001633 self.more_cc = sorted(set(self.more_cc))
1634
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001635 return results
1636
Bruce Dawson8fa42e22022-03-29 17:05:38 +00001637 def _run_check_function(self, function_name, context, sink, presubmit_path):
Scott Leecc2fe9b2020-11-19 19:38:06 +00001638 """Evaluates and returns the result of a given presubmit function.
1639
1640 If sink is given, the result of the presubmit function will be reported
1641 to the ResultSink.
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001642
1643 Args:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001644 function_name: the name of the presubmit function to evaluate
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001645 context: a context dictionary in which the function will be evaluated
Scott Leecc2fe9b2020-11-19 19:38:06 +00001646 sink: an instance of ResultSink. None, by default.
1647 Returns:
1648 the result of the presubmit function call.
1649 """
1650 start_time = time_time()
1651 try:
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001652 result = eval(function_name + '(*__args)', context)
1653 self._check_result_type(result)
Allen Webbfe7d7092021-05-18 02:05:49 +00001654 except Exception:
Bruce Dawson10a82862022-05-27 19:25:56 +00001655 _, e_value, _ = sys.exc_info()
1656 result = [
1657 OutputApi.PresubmitError(
1658 'Evaluation of %s failed: %s, %s' %
1659 (function_name, e_value, traceback.format_exc()))
1660 ]
Scott Leecc2fe9b2020-11-19 19:38:06 +00001661
Bruce Dawson2bdc49f2021-05-21 19:13:03 +00001662 elapsed_time = time_time() - start_time
1663 if elapsed_time > 10.0:
Bruce Dawson6757d462022-07-13 04:04:40 +00001664 sys.stdout.write('%6.1fs to run %s from %s.\n' %
1665 (elapsed_time, function_name, presubmit_path))
Scott Leecc2fe9b2020-11-19 19:38:06 +00001666 if sink:
Erik Staab9f38b632022-10-31 14:05:24 +00001667 failure_reason = None
Scott Leecc2fe9b2020-11-19 19:38:06 +00001668 status = rdb_wrapper.STATUS_PASS
1669 if any(r.fatal for r in result):
1670 status = rdb_wrapper.STATUS_FAIL
Erik Staab9f38b632022-10-31 14:05:24 +00001671 failure_reasons = []
1672 for r in result:
1673 fields = r.json_format()
1674 message = fields['message']
1675 items = '\n'.join(' %s' % item for item in fields['items'])
1676 failure_reasons.append('\n'.join([message, items]))
1677 if failure_reasons:
1678 failure_reason = '\n'.join(failure_reasons)
1679 sink.report(function_name, status, elapsed_time, failure_reason)
Scott Leecc2fe9b2020-11-19 19:38:06 +00001680
1681 return result
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001682
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001683 def _check_result_type(self, result):
1684 """Helper function which ensures result is a list, and all elements are
1685 instances of OutputApi.PresubmitResult"""
1686 if not isinstance(result, (tuple, list)):
1687 raise PresubmitFailure('Presubmit functions must return a tuple or list')
1688 if not all(isinstance(res, OutputApi.PresubmitResult) for res in result):
1689 raise PresubmitFailure(
1690 'All presubmit results must be of types derived from '
1691 'output_api.PresubmitResult')
1692
1693
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001694def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001695 committing,
1696 verbose,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001697 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001698 may_prompt,
Aaron Gable668c1d82018-04-03 10:19:16 -07001699 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04001700 dry_run=None,
Debrian Figueroadd2737e2019-06-21 23:50:13 +00001701 parallel=False,
Dirk Pranke6f0df682021-06-25 00:42:33 +00001702 json_output=None,
Bruce Dawson09c0c072022-05-26 20:28:58 +00001703 no_diffs=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001704 """Runs all presubmit checks that apply to the files in the change.
1705
1706 This finds all PRESUBMIT.py files in directories enclosing the files in the
1707 change (up to the repository root) and calls the relevant entrypoint function
1708 depending on whether the change is being committed or uploaded.
1709
1710 Prints errors, warnings and notifications. Prompts the user for warnings
1711 when needed.
1712
1713 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001714 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001715 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001716 verbose: Prints debug info.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001717 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001718 may_prompt: Enable (y/n) questions on warning or error. If False,
1719 any questions are answered with yes by default.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001720 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001721 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001722 parallel: if true, all tests specified by input_api.RunTests in all
1723 PRESUBMIT files will be run in parallel.
Bruce Dawson09c0c072022-05-26 20:28:58 +00001724 no_diffs: if true, implies that --files or --all was specified so some
1725 checks can be skipped, and some errors will be messages.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001726 Return:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001727 1 if presubmit checks failed or 0 otherwise.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001728 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001729 old_environ = os.environ
1730 try:
1731 # Make sure python subprocesses won't generate .pyc files.
1732 os.environ = os.environ.copy()
Edward Lemurb9830242019-10-30 22:19:20 +00001733 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001734
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001735 python_version = 'Python %s' % sys.version_info.major
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001736 if committing:
Dirk Pranke6fc394f2021-05-26 23:25:14 +00001737 sys.stdout.write('Running %s presubmit commit checks ...\n' %
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001738 python_version)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001739 else:
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001740 sys.stdout.write('Running %s presubmit upload checks ...\n' %
1741 python_version)
Edward Lemurecc27072020-01-06 16:42:34 +00001742 start_time = time_time()
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001743 presubmit_files = ListRelevantPresubmitFiles(
1744 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001745 if not presubmit_files and verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001746 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001747 results = []
Edward Lesmes8e282792018-04-03 18:50:29 -04001748 thread_pool = ThreadPool()
Edward Lemur7e3c67f2018-07-20 20:52:49 +00001749 executer = PresubmitExecuter(change, committing, verbose, gerrit_obj,
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00001750 dry_run, thread_pool, parallel, no_diffs)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001751 if default_presubmit:
1752 if verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001753 sys.stdout.write('Running default presubmit script.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001754 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00001755 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001756 for filename in presubmit_files:
1757 filename = os.path.abspath(filename)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001758 # Accept CRLF presubmit script.
Bruce Dawson6e70e862022-07-01 19:37:01 +00001759 presubmit_script = gclient_utils.FileRead(filename).replace('\r\n', '\n')
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00001760 if verbose:
1761 sys.stdout.write('Running %s\n' % filename)
1762 results += executer.ExecPresubmitScript(presubmit_script, filename)
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001763
Edward Lesmes8e282792018-04-03 18:50:29 -04001764 results += thread_pool.RunAsync()
1765
Edward Lemur6eb1d322020-02-27 22:20:15 +00001766 messages = {}
1767 should_prompt = False
1768 presubmits_failed = False
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001769 for result in results:
1770 if result.fatal:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001771 presubmits_failed = True
1772 messages.setdefault('ERRORS', []).append(result)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001773 elif result.should_prompt:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001774 should_prompt = True
1775 messages.setdefault('Warnings', []).append(result)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001776 else:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001777 messages.setdefault('Messages', []).append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001778
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001779 # Print the different message types in a consistent order. ERRORS go last
1780 # so that they will be most visible in the local-presubmit output.
1781 for name in ['Messages', 'Warnings', 'ERRORS']:
1782 if name in messages:
1783 items = messages[name]
Gavin Makd22bf602022-07-11 21:10:41 +00001784 sys.stdout.write('** Presubmit %s: %d **\n' % (name, len(items)))
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001785 for item in items:
1786 item.handle()
1787 sys.stdout.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001788
Edward Lemurecc27072020-01-06 16:42:34 +00001789 total_time = time_time() - start_time
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001790 if total_time > 1.0:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001791 sys.stdout.write(
Louis Romero4ca9e1c2022-03-10 10:29:11 +00001792 'Presubmit checks took %.1fs to calculate.\n' % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001793
Edward Lemur6eb1d322020-02-27 22:20:15 +00001794 if not should_prompt and not presubmits_failed:
Louis Romero4ca9e1c2022-03-10 10:29:11 +00001795 sys.stdout.write('%s presubmit checks passed.\n\n' % python_version)
Josip Sokcevic7592e0a2022-01-12 00:57:54 +00001796 elif should_prompt and not presubmits_failed:
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001797 sys.stdout.write('There were %s presubmit warnings. ' % python_version)
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001798 if may_prompt:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001799 presubmits_failed = not prompt_should_continue(
1800 'Are you sure you wish to continue? (y/N): ')
Garrett Beatyacb807e2020-08-28 18:17:24 +00001801 else:
1802 sys.stdout.write('\n')
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001803 else:
1804 sys.stdout.write('There were %s presubmit errors.\n' % python_version)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001805
Edward Lemur1dc66e12020-02-21 21:36:34 +00001806 if json_output:
1807 # Write the presubmit results to json output
1808 presubmit_results = {
1809 'errors': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001810 error.json_format()
1811 for error in messages.get('ERRORS', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001812 ],
1813 'notifications': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001814 notification.json_format()
1815 for notification in messages.get('Messages', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001816 ],
1817 'warnings': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001818 warning.json_format()
1819 for warning in messages.get('Warnings', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001820 ],
Edward Lemur6eb1d322020-02-27 22:20:15 +00001821 'more_cc': executer.more_cc,
Edward Lemur1dc66e12020-02-21 21:36:34 +00001822 }
1823
1824 gclient_utils.FileWrite(
1825 json_output, json.dumps(presubmit_results, sort_keys=True))
1826
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001827 global _ASKED_FOR_FEEDBACK
1828 # Ask for feedback one time out of 5.
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001829 if (results and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
Edward Lemur6eb1d322020-02-27 22:20:15 +00001830 sys.stdout.write(
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001831 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1832 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1833 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001834 _ASKED_FOR_FEEDBACK = True
Edward Lemur6eb1d322020-02-27 22:20:15 +00001835
1836 return 1 if presubmits_failed else 0
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001837 finally:
1838 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001839
1840
Edward Lemur50984a62020-02-06 18:10:18 +00001841def _scan_sub_dirs(mask, recursive):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001842 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001843 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001844
1845 results = []
1846 for root, dirs, files in os.walk('.'):
1847 if '.svn' in dirs:
1848 dirs.remove('.svn')
1849 if '.git' in dirs:
1850 dirs.remove('.git')
1851 for name in files:
1852 if fnmatch.fnmatch(name, mask):
1853 results.append(os.path.join(root, name))
1854 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001855
1856
Edward Lemur50984a62020-02-06 18:10:18 +00001857def _parse_files(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001858 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001859 files = []
1860 for arg in args:
Edward Lemur50984a62020-02-06 18:10:18 +00001861 files.extend([('M', f) for f in _scan_sub_dirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001862 return files
1863
1864
Edward Lemur50984a62020-02-06 18:10:18 +00001865def _parse_change(parser, options):
1866 """Process change options.
1867
1868 Args:
1869 parser: The parser used to parse the arguments from command line.
1870 options: The arguments parsed from command line.
1871 Returns:
Josip Sokcevic7958e302023-03-01 23:02:21 +00001872 A GitChange if the change root is a git repository, or a Change otherwise.
Edward Lemur50984a62020-02-06 18:10:18 +00001873 """
1874 if options.files and options.all_files:
1875 parser.error('<files> cannot be specified when --all-files is set.')
1876
Edward Lesmes44ea3ff2020-02-05 00:14:30 +00001877 change_scm = scm.determine_scm(options.root)
Edward Lemur50984a62020-02-06 18:10:18 +00001878 if change_scm != 'git' and not options.files:
1879 parser.error('<files> is not optional for unversioned directories.')
1880
1881 if options.files:
Josip Sokcevic017544d2022-03-31 23:47:53 +00001882 if options.source_controlled_only:
1883 # Get the filtered set of files from SCM.
1884 change_files = []
1885 for name in scm.GIT.GetAllFiles(options.root):
1886 for mask in options.files:
1887 if fnmatch.fnmatch(name, mask):
1888 change_files.append(('M', name))
1889 break
1890 else:
1891 # Get the filtered set of files from a directory scan.
1892 change_files = _parse_files(options.files, options.recursive)
Edward Lemur50984a62020-02-06 18:10:18 +00001893 elif options.all_files:
1894 change_files = [('M', f) for f in scm.GIT.GetAllFiles(options.root)]
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001895 else:
Edward Lemur50984a62020-02-06 18:10:18 +00001896 change_files = scm.GIT.CaptureStatus(
Edward Lemur7f6dec02020-02-06 20:23:58 +00001897 options.root, options.upstream or None)
Edward Lemur50984a62020-02-06 18:10:18 +00001898
1899 logging.info('Found %d file(s).', len(change_files))
1900
Josip Sokcevic7958e302023-03-01 23:02:21 +00001901 change_class = GitChange if change_scm == 'git' else Change
Edward Lemur50984a62020-02-06 18:10:18 +00001902 return change_class(
1903 options.name,
1904 options.description,
1905 options.root,
1906 change_files,
1907 options.issue,
1908 options.patchset,
1909 options.author,
1910 upstream=options.upstream)
1911
1912
1913def _parse_gerrit_options(parser, options):
1914 """Process gerrit options.
1915
1916 SIDE EFFECTS: Modifies options.author and options.description from Gerrit if
1917 options.gerrit_fetch is set.
1918
1919 Args:
1920 parser: The parser used to parse the arguments from command line.
1921 options: The arguments parsed from command line.
1922 Returns:
Stephen Martinisfb09de22021-02-25 03:41:13 +00001923 A GerritAccessor object if options.gerrit_url is set, or None otherwise.
Edward Lemur50984a62020-02-06 18:10:18 +00001924 """
1925 gerrit_obj = None
Stephen Martinisfb09de22021-02-25 03:41:13 +00001926 if options.gerrit_url:
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001927 gerrit_obj = GerritAccessor(
1928 url=options.gerrit_url,
1929 project=options.gerrit_project,
1930 branch=options.gerrit_branch)
Edward Lemur50984a62020-02-06 18:10:18 +00001931
1932 if not options.gerrit_fetch:
1933 return gerrit_obj
1934
1935 if not options.gerrit_url or not options.issue or not options.patchset:
1936 parser.error(
1937 '--gerrit_fetch requires --gerrit_url, --issue and --patchset.')
1938
1939 options.author = gerrit_obj.GetChangeOwner(options.issue)
1940 options.description = gerrit_obj.GetChangeDescription(
1941 options.issue, options.patchset)
1942
1943 logging.info('Got author: "%s"', options.author)
1944 logging.info('Got description: """\n%s\n"""', options.description)
1945
1946 return gerrit_obj
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001947
1948
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001949@contextlib.contextmanager
1950def canned_check_filter(method_names):
1951 filtered = {}
1952 try:
1953 for method_name in method_names:
1954 if not hasattr(presubmit_canned_checks, method_name):
Gavin Make6a62332020-12-04 21:57:10 +00001955 logging.warning('Skipping unknown "canned" check %s' % method_name)
Aaron Gableecee74c2018-04-02 15:13:08 -07001956 continue
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001957 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1958 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1959 yield
1960 finally:
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001961 for name, method in filtered.items():
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001962 setattr(presubmit_canned_checks, name, method)
1963
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001964
sbc@chromium.org013731e2015-02-26 18:28:43 +00001965def main(argv=None):
Edward Lemura5799e32020-01-17 19:26:51 +00001966 parser = argparse.ArgumentParser(usage='%(prog)s [options] <files...>')
1967 hooks = parser.add_mutually_exclusive_group()
1968 hooks.add_argument('-c', '--commit', action='store_true',
1969 help='Use commit instead of upload checks.')
1970 hooks.add_argument('-u', '--upload', action='store_false', dest='commit',
1971 help='Use upload instead of commit checks.')
Edward Lemur75526302020-02-27 22:31:05 +00001972 hooks.add_argument('--post_upload', action='store_true',
1973 help='Run post-upload commit hooks.')
Edward Lemura5799e32020-01-17 19:26:51 +00001974 parser.add_argument('-r', '--recursive', action='store_true',
1975 help='Act recursively.')
1976 parser.add_argument('-v', '--verbose', action='count', default=0,
1977 help='Use 2 times for more debug info.')
1978 parser.add_argument('--name', default='no name')
1979 parser.add_argument('--author')
Edward Lemur227d5102020-02-25 23:45:35 +00001980 desc = parser.add_mutually_exclusive_group()
1981 desc.add_argument('--description', default='', help='The change description.')
1982 desc.add_argument('--description_file',
1983 help='File to read change description from.')
Edward Lemura5799e32020-01-17 19:26:51 +00001984 parser.add_argument('--issue', type=int, default=0)
1985 parser.add_argument('--patchset', type=int, default=0)
1986 parser.add_argument('--root', default=os.getcwd(),
1987 help='Search for PRESUBMIT.py up to this directory. '
1988 'If inherit-review-settings-ok is present in this '
1989 'directory, parent directories up to the root file '
1990 'system directories will also be searched.')
1991 parser.add_argument('--upstream',
1992 help='Git only: the base ref or upstream branch against '
1993 'which the diff should be computed.')
1994 parser.add_argument('--default_presubmit')
1995 parser.add_argument('--may_prompt', action='store_true', default=False)
1996 parser.add_argument('--skip_canned', action='append', default=[],
1997 help='A list of checks to skip which appear in '
1998 'presubmit_canned_checks. Can be provided multiple times '
1999 'to skip multiple canned checks.')
2000 parser.add_argument('--dry_run', action='store_true', help=argparse.SUPPRESS)
2001 parser.add_argument('--gerrit_url', help=argparse.SUPPRESS)
Edward Lesmeseb1bd622021-03-01 19:54:07 +00002002 parser.add_argument('--gerrit_project', help=argparse.SUPPRESS)
2003 parser.add_argument('--gerrit_branch', help=argparse.SUPPRESS)
Edward Lemura5799e32020-01-17 19:26:51 +00002004 parser.add_argument('--gerrit_fetch', action='store_true',
2005 help=argparse.SUPPRESS)
2006 parser.add_argument('--parallel', action='store_true',
2007 help='Run all tests specified by input_api.RunTests in '
2008 'all PRESUBMIT files in parallel.')
2009 parser.add_argument('--json_output',
2010 help='Write presubmit errors to json output.')
Edward Lemur1dc66e12020-02-21 21:36:34 +00002011 parser.add_argument('--all_files', action='store_true',
Edward Lemur50984a62020-02-06 18:10:18 +00002012 help='Mark all files under source control as modified.')
Bruce Dawson09c0c072022-05-26 20:28:58 +00002013
Edward Lemura5799e32020-01-17 19:26:51 +00002014 parser.add_argument('files', nargs='*',
2015 help='List of files to be marked as modified when '
2016 'executing presubmit or post-upload hooks. fnmatch '
2017 'wildcards can also be used.')
Josip Sokcevic017544d2022-03-31 23:47:53 +00002018 parser.add_argument('--source_controlled_only', action='store_true',
2019 help='Constrain \'files\' to those in source control.')
Bruce Dawson09c0c072022-05-26 20:28:58 +00002020 parser.add_argument('--no_diffs', action='store_true',
2021 help='Assume that all "modified" files have no diffs.')
Edward Lemura5799e32020-01-17 19:26:51 +00002022 options = parser.parse_args(argv)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00002023
Erik Staabcca5c492020-04-16 17:40:07 +00002024 log_level = logging.ERROR
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002025 if options.verbose >= 2:
Erik Staabcca5c492020-04-16 17:40:07 +00002026 log_level = logging.DEBUG
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002027 elif options.verbose:
Erik Staabcca5c492020-04-16 17:40:07 +00002028 log_level = logging.INFO
2029 log_format = ('[%(levelname).1s%(asctime)s %(process)d %(thread)d '
2030 '%(filename)s] %(message)s')
2031 logging.basicConfig(format=log_format, level=log_level)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00002032
Bruce Dawsondca14bc2022-09-15 20:59:38 +00002033 # Print call stacks when _PresubmitResult objects are created with -v -v is
2034 # specified. This helps track down where presubmit messages are coming from.
2035 if options.verbose >= 2:
2036 global _SHOW_CALLSTACKS
2037 _SHOW_CALLSTACKS = True
2038
Edward Lemur227d5102020-02-25 23:45:35 +00002039 if options.description_file:
2040 options.description = gclient_utils.FileRead(options.description_file)
Edward Lemur50984a62020-02-06 18:10:18 +00002041 gerrit_obj = _parse_gerrit_options(parser, options)
2042 change = _parse_change(parser, options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00002043
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002044 try:
Edward Lemur75526302020-02-27 22:31:05 +00002045 if options.post_upload:
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00002046 return DoPostUploadExecuter(change, gerrit_obj, options.verbose)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002047 with canned_check_filter(options.skip_canned):
Edward Lemur6eb1d322020-02-27 22:20:15 +00002048 return DoPresubmitChecks(
Edward Lemur50984a62020-02-06 18:10:18 +00002049 change,
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002050 options.commit,
2051 options.verbose,
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002052 options.default_presubmit,
2053 options.may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002054 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04002055 options.dry_run,
Debrian Figueroadd2737e2019-06-21 23:50:13 +00002056 options.parallel,
Dirk Pranke6f0df682021-06-25 00:42:33 +00002057 options.json_output,
Bruce Dawson09c0c072022-05-26 20:28:58 +00002058 options.no_diffs)
Raul Tambre7c938462019-05-24 16:35:35 +00002059 except PresubmitFailure as e:
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00002060 import utils
Raul Tambre80ee78e2019-05-06 22:41:05 +00002061 print(e, file=sys.stderr)
2062 print('Maybe your depot_tools is out of date?', file=sys.stderr)
Josip Sokcevic0399e172022-03-21 23:11:51 +00002063 print('depot_tools version: %s' % utils.depot_tools_version(),
2064 file=sys.stderr)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002065 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002066
2067
2068if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00002069 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00002070 try:
2071 sys.exit(main())
2072 except KeyboardInterrupt:
2073 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07002074 sys.exit(2)