blob: b986b7bc779a8a11afd6d6c50560f027d88ac5a7 [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.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00005"""Enables directory-specific presubmit checks to run at upload and/or commit.
6"""
7
Raul Tambre80ee78e2019-05-06 22:41:05 +00008from __future__ import print_function
9
Saagar Sanghavi99816902020-08-11 22:41:25 +000010__version__ = '2.0.0'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000011
12# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
13# caching (between all different invocations of presubmit scripts for a given
14# change). We should add it as our presubmit scripts start feeling slow.
15
Edward Lemura5799e32020-01-17 19:26:51 +000016import argparse
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +090017import ast # Exposed through the API.
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +000018import contextlib
Yoshisato Yanagisawa406de132018-06-29 05:43:25 +000019import cpplint
dcheng091b7db2016-06-16 01:27:51 -070020import fnmatch # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000021import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000022import inspect
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +000023import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000024import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000025import logging
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000026import multiprocessing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000027import os # Somewhat exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000028import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000029import re # Exposed through the API.
Edward Lesmes8e282792018-04-03 18:50:29 -040030import signal
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000031import sys # Parts exposed through API.
32import tempfile # Exposed through the API.
Edward Lesmes8e282792018-04-03 18:50:29 -040033import threading
jam@chromium.org2a891dc2009-08-20 20:33:37 +000034import time
Edward Lemurde9e3ca2019-10-24 21:13:31 +000035import traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +000036import unittest # Exposed through the API.
Gavin Maka94d8fe2023-09-05 18:05:01 +000037import urllib.parse as urlparse
38import urllib.request as urllib_request
39import urllib.error as urllib_error
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000040from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000041
42# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000043import fix_encoding
Yoshisato Yanagisawa04600b42019-03-15 03:03:41 +000044import gclient_paths # Exposed through the API
45import gclient_utils
Josip Sokcevic7958e302023-03-01 23:02:21 +000046import git_footers
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000047import gerrit_util
Edward Lesmes9ce03f82021-01-12 20:13:31 +000048import owners_client
Jochen Eisinger76f5fc62017-04-07 16:27:46 +020049import owners_finder
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050import presubmit_canned_checks
Saagar Sanghavi9949ab72020-07-20 20:56:40 +000051import rdb_wrapper
Josip Sokcevic7958e302023-03-01 23:02:21 +000052import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000053import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000054
Mike Frysinger124bb8e2023-09-06 05:48:55 +000055# TODO: Should fix these warnings.
56# pylint: disable=line-too-long
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000057
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000058# Ask for feedback only once in program lifetime.
59_ASKED_FOR_FEEDBACK = False
60
Bruce Dawsondca14bc2022-09-15 20:59:38 +000061# Set if super-verbose mode is requested, for tracking where presubmit messages
62# are coming from.
63_SHOW_CALLSTACKS = False
64
65
Edward Lemurecc27072020-01-06 16:42:34 +000066def time_time():
Mike Frysinger124bb8e2023-09-06 05:48:55 +000067 # Use this so that it can be mocked in tests without interfering with python
68 # system machinery.
69 return time.time()
Edward Lemurecc27072020-01-06 16:42:34 +000070
71
maruel@chromium.org899e1c12011-04-07 17:03:18 +000072class PresubmitFailure(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000073 pass
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000074
75
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000076class CommandData(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000077 def __init__(self, name, cmd, kwargs, message, python3=True):
78 # The python3 argument is ignored but has to be retained because of the
79 # many callers in other repos that pass it in.
80 del python3
81 self.name = name
82 self.cmd = cmd
83 self.stdin = kwargs.get('stdin', None)
84 self.kwargs = kwargs.copy()
85 self.kwargs['stdout'] = subprocess.PIPE
86 self.kwargs['stderr'] = subprocess.STDOUT
87 self.kwargs['stdin'] = subprocess.PIPE
88 self.message = message
89 self.info = None
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000090
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000091
Edward Lesmes8e282792018-04-03 18:50:29 -040092# Adapted from
93# https://github.com/google/gtest-parallel/blob/master/gtest_parallel.py#L37
94#
95# An object that catches SIGINT sent to the Python process and notices
96# if processes passed to wait() die by SIGINT (we need to look for
97# both of those cases, because pressing Ctrl+C can result in either
98# the main process or one of the subprocesses getting the signal).
99#
100# Before a SIGINT is seen, wait(p) will simply call p.wait() and
101# return the result. Once a SIGINT has been seen (in the main process
102# or a subprocess, including the one the current call is waiting for),
Edward Lemur9a5bb612019-09-26 02:01:52 +0000103# wait(p) will call p.terminate().
Edward Lesmes8e282792018-04-03 18:50:29 -0400104class SigintHandler(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000105 sigint_returncodes = {
106 -signal.SIGINT, # Unix
107 -1073741510, # Windows
108 }
Edward Lesmes8e282792018-04-03 18:50:29 -0400109
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000110 def __init__(self):
111 self.__lock = threading.Lock()
112 self.__processes = set()
113 self.__got_sigint = False
114 self.__previous_signal = signal.signal(signal.SIGINT, self.interrupt)
Edward Lesmes8e282792018-04-03 18:50:29 -0400115
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000116 def __on_sigint(self):
117 self.__got_sigint = True
118 while self.__processes:
119 try:
120 self.__processes.pop().terminate()
121 except OSError:
122 pass
Edward Lesmes8e282792018-04-03 18:50:29 -0400123
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000124 def interrupt(self, signal_num, frame):
125 with self.__lock:
126 self.__on_sigint()
127 self.__previous_signal(signal_num, frame)
Edward Lesmes8e282792018-04-03 18:50:29 -0400128
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000129 def got_sigint(self):
130 with self.__lock:
131 return self.__got_sigint
132
133 def wait(self, p, stdin):
134 with self.__lock:
135 if self.__got_sigint:
136 p.terminate()
137 self.__processes.add(p)
138 stdout, stderr = p.communicate(stdin)
139 code = p.returncode
140 with self.__lock:
141 self.__processes.discard(p)
142 if code in self.sigint_returncodes:
143 self.__on_sigint()
144 return stdout, stderr
145
Edward Lesmes8e282792018-04-03 18:50:29 -0400146
147sigint_handler = SigintHandler()
148
149
Edward Lemurecc27072020-01-06 16:42:34 +0000150class Timer(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000151 def __init__(self, timeout, fn):
152 self.completed = False
153 self._fn = fn
154 self._timer = threading.Timer(timeout,
155 self._onTimer) if timeout else None
Edward Lemurecc27072020-01-06 16:42:34 +0000156
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000157 def __enter__(self):
158 if self._timer:
159 self._timer.start()
160 return self
Edward Lemurecc27072020-01-06 16:42:34 +0000161
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000162 def __exit__(self, _type, _value, _traceback):
163 if self._timer:
164 self._timer.cancel()
Edward Lemurecc27072020-01-06 16:42:34 +0000165
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000166 def _onTimer(self):
167 self._fn()
168 self.completed = True
Edward Lemurecc27072020-01-06 16:42:34 +0000169
170
Edward Lesmes8e282792018-04-03 18:50:29 -0400171class ThreadPool(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000172 def __init__(self, pool_size=None, timeout=None):
173 self.timeout = timeout
174 self._pool_size = pool_size or multiprocessing.cpu_count()
175 if sys.platform == 'win32':
176 # TODO(crbug.com/1190269) - we can't use more than 56 child
177 # processes on Windows or Python3 may hang.
178 self._pool_size = min(self._pool_size, 56)
179 self._messages = []
180 self._messages_lock = threading.Lock()
181 self._tests = []
182 self._tests_lock = threading.Lock()
183 self._nonparallel_tests = []
Edward Lesmes8e282792018-04-03 18:50:29 -0400184
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000185 def _GetCommand(self, test):
186 vpython = 'vpython3'
187 if sys.platform == 'win32':
188 vpython += '.bat'
Edward Lesmes8e282792018-04-03 18:50:29 -0400189
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000190 cmd = test.cmd
191 if cmd[0] == 'python':
192 cmd = list(cmd)
193 cmd[0] = vpython
194 elif cmd[0].endswith('.py'):
195 cmd = [vpython] + cmd
Edward Lesmes8e282792018-04-03 18:50:29 -0400196
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000197 # On Windows, scripts on the current directory take precedence over
198 # PATH, so that when testing depot_tools on Windows, calling
199 # `vpython.bat` will execute the copy of vpython of the depot_tools
200 # under test instead of the one in the bot. As a workaround, we run the
201 # tests from the parent directory instead.
202 if (cmd[0] == vpython and 'cwd' in test.kwargs
203 and os.path.basename(test.kwargs['cwd']) == 'depot_tools'):
204 test.kwargs['cwd'] = os.path.dirname(test.kwargs['cwd'])
205 cmd[1] = os.path.join('depot_tools', cmd[1])
Edward Lemur336e51f2019-11-14 21:42:04 +0000206
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000207 return cmd
Edward Lemurecc27072020-01-06 16:42:34 +0000208
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000209 def _RunWithTimeout(self, cmd, stdin, kwargs):
210 p = subprocess.Popen(cmd, **kwargs)
211 with Timer(self.timeout, p.terminate) as timer:
212 stdout, _ = sigint_handler.wait(p, stdin)
213 stdout = stdout.decode('utf-8', 'ignore')
214 if timer.completed:
215 stdout = 'Process timed out after %ss\n%s' % (self.timeout,
216 stdout)
217 return p.returncode, stdout
Edward Lemurecc27072020-01-06 16:42:34 +0000218
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000219 def CallCommand(self, test, show_callstack=None):
220 """Runs an external program.
Edward Lemurecc27072020-01-06 16:42:34 +0000221
Edward Lemura5799e32020-01-17 19:26:51 +0000222 This function converts invocation of .py files and invocations of 'python'
Edward Lemurecc27072020-01-06 16:42:34 +0000223 to vpython invocations.
224 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000225 cmd = self._GetCommand(test)
226 try:
227 start = time_time()
228 returncode, stdout = self._RunWithTimeout(cmd, test.stdin,
229 test.kwargs)
230 duration = time_time() - start
231 except Exception:
232 duration = time_time() - start
233 return test.message(
234 '%s\n%s exec failure (%4.2fs)\n%s' %
235 (test.name, ' '.join(cmd), duration, traceback.format_exc()),
236 show_callstack=show_callstack)
Edward Lemurde9e3ca2019-10-24 21:13:31 +0000237
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000238 if returncode != 0:
239 return test.message('%s\n%s (%4.2fs) failed\n%s' %
240 (test.name, ' '.join(cmd), duration, stdout),
241 show_callstack=show_callstack)
Edward Lemurecc27072020-01-06 16:42:34 +0000242
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000243 if test.info:
244 return test.info('%s\n%s (%4.2fs)' %
245 (test.name, ' '.join(cmd), duration),
246 show_callstack=show_callstack)
Edward Lesmes8e282792018-04-03 18:50:29 -0400247
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000248 def AddTests(self, tests, parallel=True):
249 if parallel:
250 self._tests.extend(tests)
251 else:
252 self._nonparallel_tests.extend(tests)
Edward Lesmes8e282792018-04-03 18:50:29 -0400253
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000254 def RunAsync(self):
255 self._messages = []
Edward Lesmes8e282792018-04-03 18:50:29 -0400256
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000257 def _WorkerFn():
258 while True:
259 test = None
260 with self._tests_lock:
261 if not self._tests:
262 break
263 test = self._tests.pop()
264 result = self.CallCommand(test, show_callstack=False)
265 if result:
266 with self._messages_lock:
267 self._messages.append(result)
Edward Lesmes8e282792018-04-03 18:50:29 -0400268
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000269 def _StartDaemon():
270 t = threading.Thread(target=_WorkerFn)
271 t.daemon = True
272 t.start()
273 return t
Edward Lesmes8e282792018-04-03 18:50:29 -0400274
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000275 while self._nonparallel_tests:
276 test = self._nonparallel_tests.pop()
277 result = self.CallCommand(test)
278 if result:
279 self._messages.append(result)
Edward Lesmes8e282792018-04-03 18:50:29 -0400280
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000281 if self._tests:
282 threads = [_StartDaemon() for _ in range(self._pool_size)]
283 for worker in threads:
284 worker.join()
Edward Lesmes8e282792018-04-03 18:50:29 -0400285
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000286 return self._messages
Edward Lesmes8e282792018-04-03 18:50:29 -0400287
288
Josip Sokcevica9a7eec2023-03-10 03:54:52 +0000289def normpath(path):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000290 '''Version of os.path.normpath that also changes backward slashes to
Josip Sokcevica9a7eec2023-03-10 03:54:52 +0000291 forward slashes when not running on Windows.
292 '''
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000293 # This is safe to always do because the Windows version of os.path.normpath
294 # will replace forward slashes with backward slashes.
295 path = path.replace(os.sep, '/')
296 return os.path.normpath(path)
Josip Sokcevica9a7eec2023-03-10 03:54:52 +0000297
298
Josip Sokcevic7958e302023-03-01 23:02:21 +0000299def _RightHandSideLinesImpl(affected_files):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000300 """Implements RightHandSideLines for InputApi and GclChange."""
301 for af in affected_files:
302 lines = af.ChangedContents()
303 for line in lines:
304 yield (af, line[0], line[1])
Josip Sokcevic7958e302023-03-01 23:02:21 +0000305
306
Edward Lemur6eb1d322020-02-27 22:20:15 +0000307def prompt_should_continue(prompt_string):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000308 sys.stdout.write(prompt_string)
309 sys.stdout.flush()
310 response = sys.stdin.readline().strip().lower()
311 return response in ('y', 'yes')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000312
Josip Sokcevic967cf672023-05-10 17:09:58 +0000313
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000314# Top level object so multiprocessing can pickle
315# Public access through OutputApi object.
316class _PresubmitResult(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000317 """Base class for result objects."""
318 fatal = False
319 should_prompt = False
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000320
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000321 def __init__(self, message, items=None, long_text='', show_callstack=None):
322 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000323 message: A short one-line message to indicate errors.
324 items: A list of short strings to indicate where errors occurred.
325 long_text: multi-line text output, e.g. from another tool
326 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000327 self._message = _PresubmitResult._ensure_str(message)
328 self._items = items or []
329 self._long_text = _PresubmitResult._ensure_str(long_text.rstrip())
330 if show_callstack is None:
331 show_callstack = _SHOW_CALLSTACKS
332 if show_callstack:
333 self._long_text += 'Presubmit result call stack is:\n'
334 self._long_text += ''.join(traceback.format_stack(None, 8))
Tom McKee61c72652021-07-20 11:56:32 +0000335
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000336 @staticmethod
337 def _ensure_str(val):
338 """
Gavin Maka94d8fe2023-09-05 18:05:01 +0000339 val: A "stringish" value. Can be any of str or bytes.
Tom McKee61c72652021-07-20 11:56:32 +0000340 returns: A str after applying encoding/decoding as needed.
341 Assumes/uses UTF-8 for relevant inputs/outputs.
Tom McKee61c72652021-07-20 11:56:32 +0000342 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000343 if isinstance(val, str):
344 return val
345 if isinstance(val, bytes):
346 return val.decode()
347 raise ValueError("Unknown string type %s" % type(val))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000348
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000349 def handle(self):
350 sys.stdout.write(self._message)
351 sys.stdout.write('\n')
352 for item in self._items:
353 sys.stdout.write(' ')
354 # Write separately in case it's unicode.
355 sys.stdout.write(str(item))
356 sys.stdout.write('\n')
357 if self._long_text:
358 sys.stdout.write('\n***************\n')
359 # Write separately in case it's unicode.
360 sys.stdout.write(self._long_text)
361 sys.stdout.write('\n***************\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000362
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000363 def json_format(self):
364 return {
365 'message': self._message,
366 'items': [str(item) for item in self._items],
367 'long_text': self._long_text,
368 'fatal': self.fatal
369 }
Debrian Figueroadd2737e2019-06-21 23:50:13 +0000370
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000371
372# Top level object so multiprocessing can pickle
373# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000374class _PresubmitError(_PresubmitResult):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000375 """A hard presubmit error."""
376 fatal = True
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000377
378
379# Top level object so multiprocessing can pickle
380# Public access through OutputApi object.
381class _PresubmitPromptWarning(_PresubmitResult):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000382 """An warning that prompts the user if they want to continue."""
383 should_prompt = True
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000384
385
386# Top level object so multiprocessing can pickle
387# Public access through OutputApi object.
388class _PresubmitNotifyResult(_PresubmitResult):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000389 """Just print something to the screen -- but it's not even a warning."""
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000390
391
392# Top level object so multiprocessing can pickle
393# Public access through OutputApi object.
394class _MailTextResult(_PresubmitResult):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000395 """A warning that should be included in the review request email."""
396 def __init__(self, *args, **kwargs):
397 super(_MailTextResult, self).__init__()
398 raise NotImplementedError()
399
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000400
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000401class GerritAccessor(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000402 """Limited Gerrit functionality for canned presubmit checks to work.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000403
404 To avoid excessive Gerrit calls, caches the results.
405 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000406 def __init__(self, url=None, project=None, branch=None):
407 self.host = urlparse.urlparse(url).netloc if url else None
408 self.project = project
409 self.branch = branch
410 self.cache = {}
411 self.code_owners_enabled = None
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000412
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000413 def _FetchChangeDetail(self, issue):
414 # Separate function to be easily mocked in tests.
415 try:
416 return gerrit_util.GetChangeDetail(
417 self.host, str(issue),
418 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
419 except gerrit_util.GerritError as e:
420 if e.http_status == 404:
421 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
422 'no credentials to fetch issue details' % issue)
423 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000424
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000425 def GetChangeInfo(self, issue):
426 """Returns labels and all revisions (patchsets) for this issue.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000427
428 The result is a dictionary according to Gerrit REST Api.
429 https://gerrit-review.googlesource.com/Documentation/rest-api.html
430
431 However, API isn't very clear what's inside, so see tests for example.
432 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000433 assert issue
434 cache_key = int(issue)
435 if cache_key not in self.cache:
436 self.cache[cache_key] = self._FetchChangeDetail(issue)
437 return self.cache[cache_key]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000438
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000439 def GetChangeDescription(self, issue, patchset=None):
440 """If patchset is none, fetches current patchset."""
441 info = self.GetChangeInfo(issue)
442 # info is a reference to cache. We'll modify it here adding description
443 # to it to the right patchset, if it is not yet there.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000444
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000445 # Find revision info for the patchset we want.
446 if patchset is not None:
447 for rev, rev_info in info['revisions'].items():
448 if str(rev_info['_number']) == str(patchset):
449 break
450 else:
451 raise Exception('patchset %s doesn\'t exist in issue %s' %
452 (patchset, issue))
453 else:
454 rev = info['current_revision']
455 rev_info = info['revisions'][rev]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000456
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000457 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000458
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000459 def GetDestRef(self, issue):
460 ref = self.GetChangeInfo(issue)['branch']
461 if not ref.startswith('refs/'):
462 # NOTE: it is possible to create 'refs/x' branch,
463 # aka 'refs/heads/refs/x'. However, this is ill-advised.
464 ref = 'refs/heads/%s' % ref
465 return ref
Mun Yong Jang603d01e2017-12-19 16:38:30 -0800466
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000467 def _GetApproversForLabel(self, issue, label):
468 change_info = self.GetChangeInfo(issue)
469 label_info = change_info.get('labels', {}).get(label, {})
470 values = label_info.get('values', {}).keys()
471 if not values:
472 return []
473 max_value = max(int(v) for v in values)
474 return [
475 v for v in label_info.get('all', [])
476 if v.get('value', 0) == max_value
477 ]
Edward Lesmes02d4b822020-11-11 00:37:35 +0000478
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000479 def IsBotCommitApproved(self, issue):
480 return bool(self._GetApproversForLabel(issue, 'Bot-Commit'))
Edward Lesmesc4566172021-03-19 16:55:13 +0000481
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000482 def IsOwnersOverrideApproved(self, issue):
483 return bool(self._GetApproversForLabel(issue, 'Owners-Override'))
Edward Lesmescf49cb82020-11-11 01:08:36 +0000484
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000485 def GetChangeOwner(self, issue):
486 return self.GetChangeInfo(issue)['owner']['email']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000487
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000488 def GetChangeReviewers(self, issue, approving_only=True):
489 changeinfo = self.GetChangeInfo(issue)
490 if approving_only:
491 reviewers = self._GetApproversForLabel(issue, 'Code-Review')
492 else:
493 reviewers = changeinfo.get('reviewers', {}).get('REVIEWER', [])
494 return [r.get('email') for r in reviewers]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000495
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000496 def UpdateDescription(self, description, issue):
497 gerrit_util.SetCommitMessage(self.host,
498 issue,
499 description,
500 notify='NONE')
Edward Lemure4d329c2020-02-03 20:41:18 +0000501
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000502 def IsCodeOwnersEnabledOnRepo(self):
503 if self.code_owners_enabled is None:
504 self.code_owners_enabled = gerrit_util.IsCodeOwnersEnabledOnRepo(
505 self.host, self.project)
506 return self.code_owners_enabled
Edward Lesmes8170c292021-03-19 20:04:43 +0000507
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000508
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000509class OutputApi(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000510 """An instance of OutputApi gets passed to presubmit scripts so that they
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000511 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000512 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000513 PresubmitResult = _PresubmitResult
514 PresubmitError = _PresubmitError
515 PresubmitPromptWarning = _PresubmitPromptWarning
516 PresubmitNotifyResult = _PresubmitNotifyResult
517 MailTextResult = _MailTextResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000518
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000519 def __init__(self, is_committing):
520 self.is_committing = is_committing
521 self.more_cc = []
Daniel Cheng7227d212017-11-17 08:12:37 -0800522
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000523 def AppendCC(self, cc):
524 """Appends a user to cc for this change."""
525 self.more_cc.append(cc)
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000526
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000527 def PresubmitPromptOrNotify(self, *args, **kwargs):
528 """Warn the user when uploading, but only notify if committing."""
529 if self.is_committing:
530 return self.PresubmitNotifyResult(*args, **kwargs)
531 return self.PresubmitPromptWarning(*args, **kwargs)
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000532
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000533
534class InputApi(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000535 """An instance of this object is passed to presubmit scripts so they can
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000536 know stuff about the change they're looking at.
537 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000538 # Method could be a function
539 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000540
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000541 # File extensions that are considered source files from a style guide
542 # perspective. Don't modify this list from a presubmit script!
543 #
544 # Files without an extension aren't included in the list. If you want to
545 # filter them as source files, add r'(^|.*?[\\\/])[^.]+$' to the allow list.
546 # Note that ALL CAPS files are skipped in DEFAULT_FILES_TO_SKIP below.
547 DEFAULT_FILES_TO_CHECK = (
548 # C++ and friends
549 r'.+\.c$',
550 r'.+\.cc$',
551 r'.+\.cpp$',
552 r'.+\.h$',
553 r'.+\.m$',
554 r'.+\.mm$',
555 r'.+\.inl$',
556 r'.+\.asm$',
557 r'.+\.hxx$',
558 r'.+\.hpp$',
559 r'.+\.s$',
560 r'.+\.S$',
561 # Scripts
562 r'.+\.js$',
563 r'.+\.ts$',
564 r'.+\.py$',
565 r'.+\.sh$',
566 r'.+\.rb$',
567 r'.+\.pl$',
568 r'.+\.pm$',
569 # Other
570 r'.+\.java$',
571 r'.+\.mk$',
572 r'.+\.am$',
573 r'.+\.css$',
574 r'.+\.mojom$',
575 r'.+\.fidl$',
576 r'.+\.rs$',
577 )
maruel@chromium.org3410d912009-06-09 20:56:16 +0000578
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000579 # Path regexp that should be excluded from being considered containing
580 # source files. Don't modify this list from a presubmit script!
581 DEFAULT_FILES_TO_SKIP = (
582 r'testing_support[\\\/]google_appengine[\\\/].*',
583 r'.*\bexperimental[\\\/].*',
584 # Exclude third_party/.* but NOT third_party/{WebKit,blink}
585 # (crbug.com/539768 and crbug.com/836555).
586 r'.*\bthird_party[\\\/](?!(WebKit|blink)[\\\/]).*',
587 # Output directories (just in case)
588 r'.*\bDebug[\\\/].*',
589 r'.*\bRelease[\\\/].*',
590 r'.*\bxcodebuild[\\\/].*',
591 r'.*\bout[\\\/].*',
592 # All caps files like README and LICENCE.
593 r'.*\b[A-Z0-9_]{2,}$',
594 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
595 r'(|.*[\\\/])\.git[\\\/].*',
596 r'(|.*[\\\/])\.svn[\\\/].*',
597 # There is no point in processing a patch file.
598 r'.+\.diff$',
599 r'.+\.patch$',
600 )
maruel@chromium.org3410d912009-06-09 20:56:16 +0000601
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000602 def __init__(self,
603 change,
604 presubmit_path,
605 is_committing,
606 verbose,
607 gerrit_obj,
608 dry_run=None,
609 thread_pool=None,
610 parallel=False,
611 no_diffs=False):
612 """Builds an InputApi object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000613
614 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000615 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000616 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000617 is_committing: True if the change is about to be committed.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000618 gerrit_obj: provides basic Gerrit codereview functionality.
619 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -0400620 parallel: if true, all tests reported via input_api.RunTests for all
621 PRESUBMIT files will be run in parallel.
Bruce Dawson09c0c072022-05-26 20:28:58 +0000622 no_diffs: if true, implies that --files or --all was specified so some
623 checks can be skipped, and some errors will be messages.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000624 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000625 # Version number of the presubmit_support script.
626 self.version = [int(x) for x in __version__.split('.')]
627 self.change = change
628 self.is_committing = is_committing
629 self.gerrit = gerrit_obj
630 self.dry_run = dry_run
631 self.no_diffs = no_diffs
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000632
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000633 self.parallel = parallel
634 self.thread_pool = thread_pool or ThreadPool()
Edward Lesmes8e282792018-04-03 18:50:29 -0400635
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000636 # We expose various modules and functions as attributes of the input_api
637 # so that presubmit scripts don't have to import them.
638 self.ast = ast
639 self.basename = os.path.basename
640 self.cpplint = cpplint
641 self.fnmatch = fnmatch
642 self.gclient_paths = gclient_paths
643 self.glob = glob.glob
644 self.json = json
645 self.logging = logging.getLogger('PRESUBMIT')
646 self.os_listdir = os.listdir
647 self.os_path = os.path
648 self.os_stat = os.stat
649 self.os_walk = os.walk
650 self.re = re
651 self.subprocess = subprocess
652 self.sys = sys
653 self.tempfile = tempfile
654 self.time = time
655 self.unittest = unittest
656 self.urllib_request = urllib_request
657 self.urllib_error = urllib_error
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000658
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000659 self.is_windows = sys.platform == 'win32'
Robert Iannucci50258932018-03-19 10:30:59 -0700660
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000661 # Set python_executable to 'vpython3' in order to allow scripts in other
662 # repos (e.g. src.git) to automatically pick up that repo's .vpython
663 # file, instead of inheriting the one in depot_tools.
664 self.python_executable = 'vpython3'
665 # Offer a python 3 executable for use during the migration off of python
666 # 2.
667 self.python3_executable = 'vpython3'
668 self.environ = os.environ
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000669
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000670 # InputApi.platform is the platform you're currently running on.
671 self.platform = sys.platform
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000672
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000673 self.cpu_count = multiprocessing.cpu_count()
674 if self.is_windows:
675 # TODO(crbug.com/1190269) - we can't use more than 56 child
676 # processes on Windows or Python3 may hang.
677 self.cpu_count = min(self.cpu_count, 56)
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000678
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000679 # The local path of the currently-being-processed presubmit script.
680 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000681
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000682 # We carry the canned checks so presubmit scripts can easily use them.
683 self.canned_checks = presubmit_canned_checks
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000685 # Temporary files we must manually remove at the end of a run.
686 self._named_temporary_files = []
Jochen Eisinger72606f82017-04-04 10:44:18 +0200687
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000688 self.owners_client = None
689 if self.gerrit and not 'PRESUBMIT_SKIP_NETWORK' in self.environ:
690 try:
691 self.owners_client = owners_client.GetCodeOwnersClient(
692 host=self.gerrit.host,
693 project=self.gerrit.project,
694 branch=self.gerrit.branch)
695 except Exception as e:
696 print('Failed to set owners_client - %s' % str(e))
697 self.owners_finder = owners_finder.OwnersFinder
698 self.verbose = verbose
699 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000700
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000701 # Replace <hash_map> and <hash_set> as headers that need to be included
702 # with 'base/containers/hash_tables.h' instead.
703 # Access to a protected member _XX of a client class
704 # pylint: disable=protected-access
705 self.cpplint._re_pattern_templates = [
706 (a, b,
707 'base/containers/hash_tables.h') if header in ('<hash_map>',
708 '<hash_set>') else
709 (a, b, header) for (a, b, header) in cpplint._re_pattern_templates
710 ]
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000711
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000712 def SetTimeout(self, timeout):
713 self.thread_pool.timeout = timeout
Edward Lemurecc27072020-01-06 16:42:34 +0000714
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000715 def PresubmitLocalPath(self):
716 """Returns the local path of the presubmit script currently being run.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717
718 This is useful if you don't want to hard-code absolute paths in the
719 presubmit script. For example, It can be used to find another file
720 relative to the PRESUBMIT.py script, so the whole tree can be branched and
721 the presubmit script still works, without editing its content.
722 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000723 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000724
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000725 def AffectedFiles(self, include_deletes=True, file_filter=None):
726 """Same as input_api.change.AffectedFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000727 (and optionally directories) in the same directory as the current presubmit
Bruce Dawson7a0b07a2020-04-23 17:14:40 +0000728 script, or subdirectories thereof. Note that files are listed using the OS
729 path separator, so backslashes are used as separators on Windows.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000730 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000731 dir_with_slash = normpath(self.PresubmitLocalPath())
732 # normpath strips trailing path separators, so the trailing separator
733 # has to be added after the normpath call.
734 if len(dir_with_slash) > 0:
735 dir_with_slash += os.path.sep
sail@chromium.org5538e022011-05-12 17:53:16 +0000736
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000737 return list(
738 filter(
739 lambda x: normpath(x.AbsoluteLocalPath()).startswith(
740 dir_with_slash),
741 self.change.AffectedFiles(include_deletes, file_filter)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000743 def LocalPaths(self):
744 """Returns local paths of input_api.AffectedFiles()."""
745 paths = [af.LocalPath() for af in self.AffectedFiles()]
746 logging.debug('LocalPaths: %s', paths)
747 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000748
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000749 def AbsoluteLocalPaths(self):
750 """Returns absolute local paths of input_api.AffectedFiles()."""
751 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000752
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000753 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
754 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000755 in the same directory as the current presubmit script, or subdirectories
756 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000757 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000758 if include_deletes is not None:
759 warn('AffectedTestableFiles(include_deletes=%s)'
760 ' is deprecated and ignored' % str(include_deletes),
761 category=DeprecationWarning,
762 stacklevel=2)
763 # pylint: disable=consider-using-generator
764 return [
765 x for x in self.AffectedFiles(include_deletes=False, **kwargs)
766 if x.IsTestableFile()
767 ]
agable0b65e732016-11-22 09:25:46 -0800768
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000769 def AffectedTextFiles(self, include_deletes=None):
770 """An alias to AffectedTestableFiles for backwards compatibility."""
771 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000772
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000773 def FilterSourceFile(self,
774 affected_file,
775 files_to_check=None,
776 files_to_skip=None,
777 allow_list=None,
778 block_list=None):
779 """Filters out files that aren't considered 'source file'.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000780
local_bot64021412020-07-08 21:05:39 +0000781 If files_to_check or files_to_skip is None, InputApi.DEFAULT_FILES_TO_CHECK
782 and InputApi.DEFAULT_FILES_TO_SKIP is used respectively.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000783
Bruce Dawson635383f2022-09-13 16:23:18 +0000784 affected_file.LocalPath() needs to re.match an entry in the files_to_check
785 list and not re.match any entries in the files_to_skip list.
786 '/' path separators should be used in the regular expressions and will work
787 on Windows as well as other platforms.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000788
789 Note: Copy-paste this function to suit your needs or use a lambda function.
790 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000791 if files_to_check is None:
792 files_to_check = self.DEFAULT_FILES_TO_CHECK
793 if files_to_skip is None:
794 files_to_skip = self.DEFAULT_FILES_TO_SKIP
local_bot30f774e2020-06-25 18:23:34 +0000795
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000796 def Find(affected_file, items):
797 local_path = affected_file.LocalPath()
798 for item in items:
799 if self.re.match(item, local_path):
800 return True
801 # Handle the cases where the files regex only handles /, but the
802 # local path uses \.
803 if self.is_windows and self.re.match(
804 item, local_path.replace('\\', '/')):
805 return True
806 return False
maruel@chromium.org3410d912009-06-09 20:56:16 +0000807
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000808 return (Find(affected_file, files_to_check)
809 and not Find(affected_file, files_to_skip))
810
811 def AffectedSourceFiles(self, source_file):
812 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000813
814 If source_file is None, InputApi.FilterSourceFile() is used.
815 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000816 if not source_file:
817 source_file = self.FilterSourceFile
818 return list(filter(source_file, self.AffectedTestableFiles()))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000819
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000820 def RightHandSideLines(self, source_file_filter=None):
821 """An iterator over all text lines in 'new' version of changed files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000822
823 Only lists lines from new or modified text files in the change that are
824 contained by the directory of the currently executing presubmit script.
825
826 This is useful for doing line-by-line regex checks, like checking for
827 trailing whitespace.
828
829 Yields:
830 a 3 tuple:
Josip Sokcevic7958e302023-03-01 23:02:21 +0000831 the AffectedFile instance of the current file;
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000832 integer line number (1-based); and
833 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000834
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000835 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000836 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000837 files = self.AffectedSourceFiles(source_file_filter)
838 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000839
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000840 def ReadFile(self, file_item, mode='r'):
841 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000842
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000843 Deny reading anything outside the repository.
844 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000845 if isinstance(file_item, AffectedFile):
846 file_item = file_item.AbsoluteLocalPath()
847 if not file_item.startswith(self.change.RepositoryRoot()):
848 raise IOError('Access outside the repository root is denied.')
849 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000850
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000851 def CreateTemporaryFile(self, **kwargs):
852 """Returns a named temporary file that must be removed with a call to
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100853 RemoveTemporaryFiles().
854
855 All keyword arguments are forwarded to tempfile.NamedTemporaryFile(),
856 except for |delete|, which is always set to False.
857
858 Presubmit checks that need to create a temporary file and pass it for
859 reading should use this function instead of NamedTemporaryFile(), as
860 Windows fails to open a file that is already open for writing.
861
862 with input_api.CreateTemporaryFile() as f:
863 f.write('xyz')
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100864 input_api.subprocess.check_output(['script-that', '--reads-from',
865 f.name])
866
867
868 Note that callers of CreateTemporaryFile() should not worry about removing
869 any temporary file; this is done transparently by the presubmit handling
870 code.
871 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000872 if 'delete' in kwargs:
873 # Prevent users from passing |delete|; we take care of file deletion
874 # ourselves and this prevents unintuitive error messages when we
875 # pass delete=False and 'delete' is also in kwargs.
876 raise TypeError(
877 'CreateTemporaryFile() does not take a "delete" '
878 'argument, file deletion is handled automatically by '
879 'the same presubmit_support code that creates InputApi '
880 'objects.')
881 temp_file = self.tempfile.NamedTemporaryFile(delete=False, **kwargs)
882 self._named_temporary_files.append(temp_file.name)
883 return temp_file
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100884
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000885 @property
886 def tbr(self):
887 """Returns if a change is TBR'ed."""
888 return 'TBR' in self.change.tags or self.change.TBRsFromDescription()
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000889
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000890 def RunTests(self, tests_mix, parallel=True):
891 tests = []
892 msgs = []
893 for t in tests_mix:
894 if isinstance(t, OutputApi.PresubmitResult) and t:
895 msgs.append(t)
896 else:
897 assert issubclass(t.message, _PresubmitResult)
898 tests.append(t)
899 if self.verbose:
900 t.info = _PresubmitNotifyResult
901 if not t.kwargs.get('cwd'):
902 t.kwargs['cwd'] = self.PresubmitLocalPath()
903 self.thread_pool.AddTests(tests, parallel)
904 # When self.parallel is True (i.e. --parallel is passed as an option)
905 # RunTests doesn't actually run tests. It adds them to a ThreadPool that
906 # will run all tests once all PRESUBMIT files are processed.
907 # Otherwise, it will run them and return the results.
908 if not self.parallel:
909 msgs.extend(self.thread_pool.RunAsync())
910 return msgs
scottmg86099d72016-09-01 09:16:51 -0700911
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000912
Josip Sokcevic7958e302023-03-01 23:02:21 +0000913class _DiffCache(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000914 """Caches diffs retrieved from a particular SCM."""
915 def __init__(self, upstream=None):
916 """Stores the upstream revision against which all diffs will be computed."""
917 self._upstream = upstream
Josip Sokcevic7958e302023-03-01 23:02:21 +0000918
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000919 def GetDiff(self, path, local_root):
920 """Get the diff for a particular path."""
921 raise NotImplementedError()
Josip Sokcevic7958e302023-03-01 23:02:21 +0000922
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000923 def GetOldContents(self, path, local_root):
924 """Get the old version for a particular path."""
925 raise NotImplementedError()
Josip Sokcevic7958e302023-03-01 23:02:21 +0000926
927
928class _GitDiffCache(_DiffCache):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000929 """DiffCache implementation for git; gets all file diffs at once."""
930 def __init__(self, upstream):
931 super(_GitDiffCache, self).__init__(upstream=upstream)
932 self._diffs_by_file = None
Josip Sokcevic7958e302023-03-01 23:02:21 +0000933
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000934 def GetDiff(self, path, local_root):
935 # Compare against None to distinguish between None and an initialized
936 # but empty dictionary.
937 if self._diffs_by_file == None:
938 # Compute a single diff for all files and parse the output; should
939 # with git this is much faster than computing one diff for each
940 # file.
941 diffs = {}
Josip Sokcevic7958e302023-03-01 23:02:21 +0000942
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000943 # Don't specify any filenames below, because there are command line
944 # length limits on some platforms and GenerateDiff would fail.
945 unified_diff = scm.GIT.GenerateDiff(local_root,
946 files=[],
947 full_move=True,
948 branch=self._upstream)
Josip Sokcevic7958e302023-03-01 23:02:21 +0000949
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000950 # This regex matches the path twice, separated by a space. Note that
951 # filename itself may contain spaces.
952 file_marker = re.compile(
953 '^diff --git (?P<filename>.*) (?P=filename)$')
954 current_diff = []
955 keep_line_endings = True
956 for x in unified_diff.splitlines(keep_line_endings):
957 match = file_marker.match(x)
958 if match:
959 # Marks the start of a new per-file section.
960 diffs[match.group('filename')] = current_diff = [x]
961 elif x.startswith('diff --git'):
962 raise PresubmitFailure('Unexpected diff line: %s' % x)
963 else:
964 current_diff.append(x)
Josip Sokcevic7958e302023-03-01 23:02:21 +0000965
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000966 self._diffs_by_file = dict(
967 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
Josip Sokcevic7958e302023-03-01 23:02:21 +0000968
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000969 if path not in self._diffs_by_file:
970 # SCM didn't have any diff on this file. It could be that the file
971 # was not modified at all (e.g. user used --all flag in git cl
972 # presubmit). Intead of failing, return empty string. See:
973 # https://crbug.com/808346.
974 return ''
Josip Sokcevic7958e302023-03-01 23:02:21 +0000975
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000976 return self._diffs_by_file[path]
Josip Sokcevic7958e302023-03-01 23:02:21 +0000977
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000978 def GetOldContents(self, path, local_root):
979 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
Josip Sokcevic7958e302023-03-01 23:02:21 +0000980
981
982class AffectedFile(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000983 """Representation of a file in a change."""
Josip Sokcevic7958e302023-03-01 23:02:21 +0000984
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000985 DIFF_CACHE = _DiffCache
Josip Sokcevic7958e302023-03-01 23:02:21 +0000986
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000987 # Method could be a function
988 # pylint: disable=no-self-use
989 def __init__(self, path, action, repository_root, diff_cache):
990 self._path = path
991 self._action = action
992 self._local_root = repository_root
993 self._is_directory = None
994 self._cached_changed_contents = None
995 self._cached_new_contents = None
996 self._diff_cache = diff_cache
997 logging.debug('%s(%s)', self.__class__.__name__, self._path)
Josip Sokcevic7958e302023-03-01 23:02:21 +0000998
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000999 def LocalPath(self):
1000 """Returns the path of this file on the local disk relative to client root.
Josip Sokcevic7958e302023-03-01 23:02:21 +00001001
1002 This should be used for error messages but not for accessing files,
1003 because presubmit checks are run with CWD=PresubmitLocalPath() (which is
1004 often != client root).
1005 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001006 return normpath(self._path)
Josip Sokcevic7958e302023-03-01 23:02:21 +00001007
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001008 def AbsoluteLocalPath(self):
1009 """Returns the absolute path of this file on the local disk.
Josip Sokcevic7958e302023-03-01 23:02:21 +00001010 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001011 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
Josip Sokcevic7958e302023-03-01 23:02:21 +00001012
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001013 def Action(self):
1014 """Returns the action on this opened file, e.g. A, M, D, etc."""
1015 return self._action
Josip Sokcevic7958e302023-03-01 23:02:21 +00001016
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001017 def IsTestableFile(self):
1018 """Returns True if the file is a text file and not a binary file.
Josip Sokcevic7958e302023-03-01 23:02:21 +00001019
1020 Deleted files are not text file."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001021 raise NotImplementedError() # Implement when needed
Josip Sokcevic7958e302023-03-01 23:02:21 +00001022
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001023 def IsTextFile(self):
1024 """An alias to IsTestableFile for backwards compatibility."""
1025 return self.IsTestableFile()
Josip Sokcevic7958e302023-03-01 23:02:21 +00001026
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001027 def OldContents(self):
1028 """Returns an iterator over the lines in the old version of file.
Josip Sokcevic7958e302023-03-01 23:02:21 +00001029
1030 The old version is the file before any modifications in the user's
1031 workspace, i.e. the 'left hand side'.
1032
1033 Contents will be empty if the file is a directory or does not exist.
1034 Note: The carriage returns (LF or CR) are stripped off.
1035 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001036 return self._diff_cache.GetOldContents(self.LocalPath(),
1037 self._local_root).splitlines()
Josip Sokcevic7958e302023-03-01 23:02:21 +00001038
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001039 def NewContents(self):
1040 """Returns an iterator over the lines in the new version of file.
Josip Sokcevic7958e302023-03-01 23:02:21 +00001041
1042 The new version is the file in the user's workspace, i.e. the 'right hand
1043 side'.
1044
1045 Contents will be empty if the file is a directory or does not exist.
1046 Note: The carriage returns (LF or CR) are stripped off.
1047 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001048 if self._cached_new_contents is None:
1049 self._cached_new_contents = []
1050 try:
1051 self._cached_new_contents = gclient_utils.FileRead(
1052 self.AbsoluteLocalPath(), 'rU').splitlines()
1053 except IOError:
1054 pass # File not found? That's fine; maybe it was deleted.
1055 except UnicodeDecodeError as e:
1056 # log the filename since we're probably trying to read a binary
1057 # file, and shouldn't be.
1058 print('Error reading %s: %s' % (self.AbsoluteLocalPath(), e))
1059 raise
Josip Sokcevic7958e302023-03-01 23:02:21 +00001060
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001061 return self._cached_new_contents[:]
Josip Sokcevic7958e302023-03-01 23:02:21 +00001062
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001063 def ChangedContents(self, keeplinebreaks=False):
1064 """Returns a list of tuples (line number, line text) of all new lines.
Josip Sokcevic7958e302023-03-01 23:02:21 +00001065
1066 This relies on the scm diff output describing each changed code section
1067 with a line of the form
1068
1069 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
1070 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001071 # Don't return cached results when line breaks are requested.
1072 if not keeplinebreaks and self._cached_changed_contents is not None:
1073 return self._cached_changed_contents[:]
1074 result = []
1075 line_num = 0
Josip Sokcevic7958e302023-03-01 23:02:21 +00001076
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001077 # The keeplinebreaks parameter to splitlines must be True or else the
1078 # CheckForWindowsLineEndings presubmit will be a NOP.
1079 for line in self.GenerateScmDiff().splitlines(keeplinebreaks):
1080 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
1081 if m:
1082 line_num = int(m.groups(1)[0])
1083 continue
1084 if line.startswith('+') and not line.startswith('++'):
1085 result.append((line_num, line[1:]))
1086 if not line.startswith('-'):
1087 line_num += 1
1088 # Don't cache results with line breaks.
1089 if keeplinebreaks:
1090 return result
1091 self._cached_changed_contents = result
1092 return self._cached_changed_contents[:]
Josip Sokcevic7958e302023-03-01 23:02:21 +00001093
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001094 def __str__(self):
1095 return self.LocalPath()
Josip Sokcevic7958e302023-03-01 23:02:21 +00001096
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001097 def GenerateScmDiff(self):
1098 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
Josip Sokcevic7958e302023-03-01 23:02:21 +00001099
1100
1101class GitAffectedFile(AffectedFile):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001102 """Representation of a file in a change out of a git checkout."""
1103 # Method 'NNN' is abstract in class 'NNN' but is not overridden
1104 # pylint: disable=abstract-method
Josip Sokcevic7958e302023-03-01 23:02:21 +00001105
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001106 DIFF_CACHE = _GitDiffCache
Josip Sokcevic7958e302023-03-01 23:02:21 +00001107
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001108 def __init__(self, *args, **kwargs):
1109 AffectedFile.__init__(self, *args, **kwargs)
1110 self._server_path = None
1111 self._is_testable_file = None
Josip Sokcevic7958e302023-03-01 23:02:21 +00001112
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001113 def IsTestableFile(self):
1114 if self._is_testable_file is None:
1115 if self.Action() == 'D':
1116 # A deleted file is not testable.
1117 self._is_testable_file = False
1118 else:
1119 self._is_testable_file = os.path.isfile(
1120 self.AbsoluteLocalPath())
1121 return self._is_testable_file
Josip Sokcevic7958e302023-03-01 23:02:21 +00001122
1123
1124class Change(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001125 """Describe a change.
Josip Sokcevic7958e302023-03-01 23:02:21 +00001126
1127 Used directly by the presubmit scripts to query the current change being
1128 tested.
1129
1130 Instance members:
1131 tags: Dictionary of KEY=VALUE pairs found in the change description.
1132 self.KEY: equivalent to tags['KEY']
1133 """
1134
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001135 _AFFECTED_FILES = AffectedFile
Josip Sokcevic7958e302023-03-01 23:02:21 +00001136
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001137 # Matches key/value (or 'tag') lines in changelist descriptions.
1138 TAG_LINE_RE = re.compile(
1139 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
1140 scm = ''
Josip Sokcevic7958e302023-03-01 23:02:21 +00001141
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001142 def __init__(self,
1143 name,
1144 description,
1145 local_root,
1146 files,
1147 issue,
1148 patchset,
1149 author,
1150 upstream=None):
1151 if files is None:
1152 files = []
1153 self._name = name
1154 # Convert root into an absolute path.
1155 self._local_root = os.path.abspath(local_root)
1156 self._upstream = upstream
1157 self.issue = issue
1158 self.patchset = patchset
1159 self.author_email = author
Josip Sokcevic7958e302023-03-01 23:02:21 +00001160
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001161 self._full_description = ''
1162 self.tags = {}
1163 self._description_without_tags = ''
1164 self.SetDescriptionText(description)
Josip Sokcevic7958e302023-03-01 23:02:21 +00001165
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001166 assert all((isinstance(f, (list, tuple)) and len(f) == 2)
1167 for f in files), files
Josip Sokcevic7958e302023-03-01 23:02:21 +00001168
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001169 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
1170 self._affected_files = [
1171 self._AFFECTED_FILES(path, action.strip(), self._local_root,
1172 diff_cache) for action, path in files
1173 ]
Josip Sokcevic7958e302023-03-01 23:02:21 +00001174
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001175 def UpstreamBranch(self):
1176 """Returns the upstream branch for the change."""
1177 return self._upstream
Josip Sokcevic7958e302023-03-01 23:02:21 +00001178
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001179 def Name(self):
1180 """Returns the change name."""
1181 return self._name
Josip Sokcevic7958e302023-03-01 23:02:21 +00001182
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001183 def DescriptionText(self):
1184 """Returns the user-entered changelist description, minus tags.
Josip Sokcevic7958e302023-03-01 23:02:21 +00001185
1186 Any line in the user-provided description starting with e.g. 'FOO='
1187 (whitespace permitted before and around) is considered a tag line. Such
1188 lines are stripped out of the description this function returns.
1189 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001190 return self._description_without_tags
Josip Sokcevic7958e302023-03-01 23:02:21 +00001191
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001192 def FullDescriptionText(self):
1193 """Returns the complete changelist description including tags."""
1194 return self._full_description
Josip Sokcevic7958e302023-03-01 23:02:21 +00001195
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001196 def SetDescriptionText(self, description):
1197 """Sets the full description text (including tags) to |description|.
Josip Sokcevic7958e302023-03-01 23:02:21 +00001198
1199 Also updates the list of tags."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001200 self._full_description = description
Josip Sokcevic7958e302023-03-01 23:02:21 +00001201
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001202 # From the description text, build up a dictionary of key/value pairs
1203 # plus the description minus all key/value or 'tag' lines.
1204 description_without_tags = []
1205 self.tags = {}
1206 for line in self._full_description.splitlines():
1207 m = self.TAG_LINE_RE.match(line)
1208 if m:
1209 self.tags[m.group('key')] = m.group('value')
1210 else:
1211 description_without_tags.append(line)
Josip Sokcevic7958e302023-03-01 23:02:21 +00001212
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001213 # Change back to text and remove whitespace at end.
1214 self._description_without_tags = (
1215 '\n'.join(description_without_tags).rstrip())
Josip Sokcevic7958e302023-03-01 23:02:21 +00001216
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001217 def AddDescriptionFooter(self, key, value):
1218 """Adds the given footer to the change description.
Josip Sokcevic7958e302023-03-01 23:02:21 +00001219
1220 Args:
1221 key: A string with the key for the git footer. It must conform to
1222 the git footers format (i.e. 'List-Of-Tokens') and will be case
1223 normalized so that each token is title-cased.
1224 value: A string with the value for the git footer.
1225 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001226 description = git_footers.add_footer(self.FullDescriptionText(),
1227 git_footers.normalize_name(key),
1228 value)
1229 self.SetDescriptionText(description)
Josip Sokcevic7958e302023-03-01 23:02:21 +00001230
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001231 def RepositoryRoot(self):
1232 """Returns the repository (checkout) root directory for this change,
Josip Sokcevic7958e302023-03-01 23:02:21 +00001233 as an absolute path.
1234 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001235 return self._local_root
Josip Sokcevic7958e302023-03-01 23:02:21 +00001236
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001237 def __getattr__(self, attr):
1238 """Return tags directly as attributes on the object."""
1239 if not re.match(r'^[A-Z_]*$', attr):
1240 raise AttributeError(self, attr)
1241 return self.tags.get(attr)
Josip Sokcevic7958e302023-03-01 23:02:21 +00001242
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001243 def GitFootersFromDescription(self):
1244 """Return the git footers present in the description.
Josip Sokcevic7958e302023-03-01 23:02:21 +00001245
1246 Returns:
1247 footers: A dict of {footer: [values]} containing a multimap of the footers
1248 in the change description.
1249 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001250 return git_footers.parse_footers(self.FullDescriptionText())
Josip Sokcevic7958e302023-03-01 23:02:21 +00001251
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001252 def BugsFromDescription(self):
1253 """Returns all bugs referenced in the commit description."""
1254 bug_tags = ['BUG', 'FIXED']
Josip Sokcevic7958e302023-03-01 23:02:21 +00001255
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001256 tags = []
1257 for tag in bug_tags:
1258 values = self.tags.get(tag)
1259 if values:
1260 tags += [value.strip() for value in values.split(',')]
Josip Sokcevic7958e302023-03-01 23:02:21 +00001261
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001262 footers = []
1263 parsed = self.GitFootersFromDescription()
1264 unsplit_footers = parsed.get('Bug', []) + parsed.get('Fixed', [])
1265 for unsplit_footer in unsplit_footers:
1266 footers += [b.strip() for b in unsplit_footer.split(',')]
1267 return sorted(set(tags + footers))
Josip Sokcevic7958e302023-03-01 23:02:21 +00001268
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001269 def ReviewersFromDescription(self):
1270 """Returns all reviewers listed in the commit description."""
1271 # We don't support a 'R:' git-footer for reviewers; that is in metadata.
1272 tags = [
1273 r.strip() for r in self.tags.get('R', '').split(',') if r.strip()
1274 ]
1275 return sorted(set(tags))
Josip Sokcevic7958e302023-03-01 23:02:21 +00001276
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001277 def TBRsFromDescription(self):
1278 """Returns all TBR reviewers listed in the commit description."""
1279 tags = [
1280 r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()
1281 ]
1282 # TODO(crbug.com/839208): Remove support for 'Tbr:' when TBRs are
1283 # programmatically determined by self-CR+1s.
1284 footers = self.GitFootersFromDescription().get('Tbr', [])
1285 return sorted(set(tags + footers))
Josip Sokcevic7958e302023-03-01 23:02:21 +00001286
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001287 # TODO(crbug.com/753425): Delete these once we're sure they're unused.
1288 @property
1289 def BUG(self):
1290 return ','.join(self.BugsFromDescription())
Josip Sokcevic7958e302023-03-01 23:02:21 +00001291
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001292 @property
1293 def R(self):
1294 return ','.join(self.ReviewersFromDescription())
Josip Sokcevic7958e302023-03-01 23:02:21 +00001295
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001296 @property
1297 def TBR(self):
1298 return ','.join(self.TBRsFromDescription())
1299
1300 def AllFiles(self, root=None):
1301 """List all files under source control in the repo."""
1302 raise NotImplementedError()
1303
1304 def AffectedFiles(self, include_deletes=True, file_filter=None):
1305 """Returns a list of AffectedFile instances for all files in the change.
Josip Sokcevic7958e302023-03-01 23:02:21 +00001306
1307 Args:
1308 include_deletes: If false, deleted files will be filtered out.
1309 file_filter: An additional filter to apply.
1310
1311 Returns:
1312 [AffectedFile(path, action), AffectedFile(path, action)]
1313 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001314 affected = list(filter(file_filter, self._affected_files))
Josip Sokcevic7958e302023-03-01 23:02:21 +00001315
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001316 if include_deletes:
1317 return affected
1318 return list(filter(lambda x: x.Action() != 'D', affected))
Josip Sokcevic7958e302023-03-01 23:02:21 +00001319
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001320 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
1321 """Return a list of the existing text files in a change."""
1322 if include_deletes is not None:
1323 warn('AffectedTeestableFiles(include_deletes=%s)'
1324 ' is deprecated and ignored' % str(include_deletes),
1325 category=DeprecationWarning,
1326 stacklevel=2)
1327 return list(
1328 filter(lambda x: x.IsTestableFile(),
1329 self.AffectedFiles(include_deletes=False, **kwargs)))
Josip Sokcevic7958e302023-03-01 23:02:21 +00001330
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001331 def AffectedTextFiles(self, include_deletes=None):
1332 """An alias to AffectedTestableFiles for backwards compatibility."""
1333 return self.AffectedTestableFiles(include_deletes=include_deletes)
Josip Sokcevic7958e302023-03-01 23:02:21 +00001334
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001335 def LocalPaths(self):
1336 """Convenience function."""
1337 return [af.LocalPath() for af in self.AffectedFiles()]
Josip Sokcevic7958e302023-03-01 23:02:21 +00001338
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001339 def AbsoluteLocalPaths(self):
1340 """Convenience function."""
1341 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
Josip Sokcevic7958e302023-03-01 23:02:21 +00001342
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001343 def RightHandSideLines(self):
1344 """An iterator over all text lines in 'new' version of changed files.
Josip Sokcevic7958e302023-03-01 23:02:21 +00001345
1346 Lists lines from new or modified text files in the change.
1347
1348 This is useful for doing line-by-line regex checks, like checking for
1349 trailing whitespace.
1350
1351 Yields:
1352 a 3 tuple:
1353 the AffectedFile instance of the current file;
1354 integer line number (1-based); and
1355 the contents of the line as a string.
1356 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001357 return _RightHandSideLinesImpl(
1358 x for x in self.AffectedFiles(include_deletes=False)
1359 if x.IsTestableFile())
Josip Sokcevic7958e302023-03-01 23:02:21 +00001360
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001361 def OriginalOwnersFiles(self):
1362 """A map from path names of affected OWNERS files to their old content."""
1363 def owners_file_filter(f):
1364 return 'OWNERS' in os.path.split(f.LocalPath())[1]
1365
1366 files = self.AffectedFiles(file_filter=owners_file_filter)
1367 return {f.LocalPath(): f.OldContents() for f in files}
Josip Sokcevic7958e302023-03-01 23:02:21 +00001368
1369
1370class GitChange(Change):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001371 _AFFECTED_FILES = GitAffectedFile
1372 scm = 'git'
Josip Sokcevic7958e302023-03-01 23:02:21 +00001373
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001374 def AllFiles(self, root=None):
1375 """List all files under source control in the repo."""
1376 root = root or self.RepositoryRoot()
1377 return subprocess.check_output(
1378 ['git', '-c', 'core.quotePath=false', 'ls-files', '--', '.'],
1379 cwd=root).decode('utf-8', 'ignore').splitlines()
Josip Sokcevic7958e302023-03-01 23:02:21 +00001380
1381
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001382def ListRelevantPresubmitFiles(files, root):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001383 """Finds all presubmit files that apply to a given set of source files.
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001384
1385 If inherit-review-settings-ok is present right under root, looks for
1386 PRESUBMIT.py in directories enclosing root.
1387
1388 Args:
1389 files: An iterable container containing file paths.
1390 root: Path where to stop searching.
1391
1392 Return:
1393 List of absolute paths of the existing PRESUBMIT.py scripts.
1394 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001395 files = [normpath(os.path.join(root, f)) for f in files]
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001396
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001397 # List all the individual directories containing files.
1398 directories = {os.path.dirname(f) for f in files}
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001399
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001400 # Ignore root if inherit-review-settings-ok is present.
1401 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1402 root = None
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001403
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001404 # Collect all unique directories that may contain PRESUBMIT.py.
1405 candidates = set()
1406 for directory in directories:
1407 while True:
1408 if directory in candidates:
1409 break
1410 candidates.add(directory)
1411 if directory == root:
1412 break
1413 parent_dir = os.path.dirname(directory)
1414 if parent_dir == directory:
1415 # We hit the system root directory.
1416 break
1417 directory = parent_dir
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001418
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001419 # Look for PRESUBMIT.py in all candidate directories.
1420 results = []
1421 for directory in sorted(list(candidates)):
1422 try:
1423 for f in os.listdir(directory):
1424 p = os.path.join(directory, f)
1425 if os.path.isfile(p) and re.match(
1426 r'PRESUBMIT.*\.py$',
1427 f) and not f.startswith('PRESUBMIT_test'):
1428 results.append(p)
1429 except OSError:
1430 pass
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001431
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001432 logging.debug('Presubmit files: %s', ','.join(results))
1433 return results
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00001434
1435
rmistry@google.com5626a922015-02-26 14:03:30 +00001436class GetPostUploadExecuter(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001437 def __init__(self, change, gerrit_obj):
1438 """
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001439 Args:
Pavol Marko624e7ee2023-01-09 09:56:29 +00001440 change: The Change object.
1441 gerrit_obj: provides basic Gerrit codereview functionality.
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001442 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001443 self.change = change
1444 self.gerrit = gerrit_obj
Josip Sokcevice293d3d2022-02-16 22:52:15 +00001445
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001446 def ExecPresubmitScript(self, script_text, presubmit_path):
1447 """Executes PostUploadHook() from a single presubmit script.
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001448 Caller is responsible for validating whether the hook should be executed
1449 and should only call this function if it should be.
rmistry@google.com5626a922015-02-26 14:03:30 +00001450
1451 Args:
1452 script_text: The text of the presubmit script.
1453 presubmit_path: Project script to run.
rmistry@google.com5626a922015-02-26 14:03:30 +00001454
1455 Return:
1456 A list of results objects.
1457 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001458 # Change to the presubmit file's directory to support local imports.
1459 presubmit_dir = os.path.dirname(presubmit_path)
1460 main_path = os.getcwd()
1461 try:
1462 os.chdir(presubmit_dir)
1463 return self._execute_with_local_working_directory(
1464 script_text, presubmit_dir, presubmit_path)
1465 finally:
1466 # Return the process to the original working directory.
1467 os.chdir(main_path)
Pavol Marko624e7ee2023-01-09 09:56:29 +00001468
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001469 def _execute_with_local_working_directory(self, script_text, presubmit_dir,
1470 presubmit_path):
1471 context = {}
1472 try:
1473 exec(
1474 compile(script_text, presubmit_path, 'exec', dont_inherit=True),
1475 context)
1476 except Exception as e:
1477 raise PresubmitFailure('"%s" had an exception.\n%s' %
1478 (presubmit_path, e))
rmistry@google.com5626a922015-02-26 14:03:30 +00001479
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001480 function_name = 'PostUploadHook'
1481 if function_name not in context:
1482 return {}
1483 post_upload_hook = context[function_name]
1484 if not len(inspect.getfullargspec(post_upload_hook)[0]) == 3:
1485 raise PresubmitFailure(
1486 'Expected function "PostUploadHook" to take three arguments.')
1487 return post_upload_hook(self.gerrit, self.change, OutputApi(False))
rmistry@google.com5626a922015-02-26 14:03:30 +00001488
1489
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001490def _MergeMasters(masters1, masters2):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001491 """Merges two master maps. Merges also the tests of each builder."""
1492 result = {}
1493 for (master, builders) in itertools.chain(masters1.items(),
1494 masters2.items()):
1495 new_builders = result.setdefault(master, {})
1496 for (builder, tests) in builders.items():
1497 new_builders.setdefault(builder, set([])).update(tests)
1498 return result
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001499
1500
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00001501def DoPostUploadExecuter(change, gerrit_obj, verbose):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001502 """Execute the post upload hook.
rmistry@google.com5626a922015-02-26 14:03:30 +00001503
1504 Args:
1505 change: The Change object.
Edward Lemur016a0872020-02-04 22:13:28 +00001506 gerrit_obj: The GerritAccessor object.
rmistry@google.com5626a922015-02-26 14:03:30 +00001507 verbose: Prints debug info.
rmistry@google.com5626a922015-02-26 14:03:30 +00001508 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001509 python_version = 'Python %s' % sys.version_info.major
1510 sys.stdout.write('Running %s post upload checks ...\n' % python_version)
1511 presubmit_files = ListRelevantPresubmitFiles(change.LocalPaths(),
1512 change.RepositoryRoot())
1513 if not presubmit_files and verbose:
1514 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
1515 results = []
1516 executer = GetPostUploadExecuter(change, gerrit_obj)
1517 # The root presubmit file should be executed after the ones in
1518 # subdirectories. i.e. the specific post upload hooks should run before the
1519 # general ones. Thus, reverse the order provided by
1520 # ListRelevantPresubmitFiles.
1521 presubmit_files.reverse()
rmistry@google.com5626a922015-02-26 14:03:30 +00001522
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001523 for filename in presubmit_files:
1524 filename = os.path.abspath(filename)
1525 # Accept CRLF presubmit script.
1526 presubmit_script = gclient_utils.FileRead(filename).replace(
1527 '\r\n', '\n')
1528 if verbose:
1529 sys.stdout.write('Running %s\n' % filename)
1530 results.extend(executer.ExecPresubmitScript(presubmit_script, filename))
rmistry@google.com5626a922015-02-26 14:03:30 +00001531
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001532 if not results:
1533 return 0
Edward Lemur6eb1d322020-02-27 22:20:15 +00001534
Edward Lemur6eb1d322020-02-27 22:20:15 +00001535 sys.stdout.write('\n')
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001536 sys.stdout.write('** Post Upload Hook Messages **\n')
Edward Lemur6eb1d322020-02-27 22:20:15 +00001537
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001538 exit_code = 0
1539 for result in results:
1540 if result.fatal:
1541 exit_code = 1
1542 result.handle()
1543 sys.stdout.write('\n')
1544
1545 return exit_code
1546
rmistry@google.com5626a922015-02-26 14:03:30 +00001547
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001548class PresubmitExecuter(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001549 def __init__(self,
1550 change,
1551 committing,
1552 verbose,
1553 gerrit_obj,
1554 dry_run=None,
1555 thread_pool=None,
1556 parallel=False,
1557 no_diffs=False):
1558 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001559 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001560 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001561 committing: True if 'git cl land' is running, False if 'git cl upload' is.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001562 gerrit_obj: provides basic Gerrit codereview functionality.
1563 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001564 parallel: if true, all tests reported via input_api.RunTests for all
1565 PRESUBMIT files will be run in parallel.
Bruce Dawson09c0c072022-05-26 20:28:58 +00001566 no_diffs: if true, implies that --files or --all was specified so some
1567 checks can be skipped, and some errors will be messages.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001568 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001569 self.change = change
1570 self.committing = committing
1571 self.gerrit = gerrit_obj
1572 self.verbose = verbose
1573 self.dry_run = dry_run
1574 self.more_cc = []
1575 self.thread_pool = thread_pool
1576 self.parallel = parallel
1577 self.no_diffs = no_diffs
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001578
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001579 def ExecPresubmitScript(self, script_text, presubmit_path):
1580 """Executes a single presubmit script.
Josip Sokcevic632bbc02022-05-19 05:32:50 +00001581 Caller is responsible for validating whether the hook should be executed
1582 and should only call this function if it should be.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001583
1584 Args:
1585 script_text: The text of the presubmit script.
1586 presubmit_path: The path to the presubmit file (this will be reported via
1587 input_api.PresubmitLocalPath()).
1588
1589 Return:
1590 A list of result objects, empty if no problems.
1591 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001592 # Change to the presubmit file's directory to support local imports.
1593 presubmit_dir = os.path.dirname(presubmit_path)
1594 main_path = os.getcwd()
1595 try:
1596 os.chdir(presubmit_dir)
1597 return self._execute_with_local_working_directory(
1598 script_text, presubmit_dir, presubmit_path)
1599 finally:
1600 # Return the process to the original working directory.
1601 os.chdir(main_path)
chase@chromium.org8e416c82009-10-06 04:30:44 +00001602
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001603 def _execute_with_local_working_directory(self, script_text, presubmit_dir,
1604 presubmit_path):
1605 # Load the presubmit script into context.
1606 input_api = InputApi(self.change,
1607 presubmit_path,
1608 self.committing,
1609 self.verbose,
1610 gerrit_obj=self.gerrit,
1611 dry_run=self.dry_run,
1612 thread_pool=self.thread_pool,
1613 parallel=self.parallel,
1614 no_diffs=self.no_diffs)
1615 output_api = OutputApi(self.committing)
1616 context = {}
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001617
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001618 try:
1619 exec(
1620 compile(script_text, presubmit_path, 'exec', dont_inherit=True),
1621 context)
1622 except Exception as e:
1623 raise PresubmitFailure('"%s" had an exception.\n%s' %
1624 (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001625
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001626 context['__args'] = (input_api, output_api)
Ben Pastene8351dc12020-08-06 05:01:35 +00001627
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001628 # Get path of presubmit directory relative to repository root.
1629 # Always use forward slashes, so that path is same in *nix and Windows
1630 root = input_api.change.RepositoryRoot()
1631 rel_path = os.path.relpath(presubmit_dir, root)
1632 rel_path = rel_path.replace(os.path.sep, '/')
Ben Pastene8351dc12020-08-06 05:01:35 +00001633
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001634 # Get the URL of git remote origin and use it to identify host and
1635 # project
1636 host = project = ''
1637 if self.gerrit:
1638 host = self.gerrit.host or ''
1639 project = self.gerrit.project or ''
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001640
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001641 # Prefix for test names
1642 prefix = 'presubmit:%s/%s:%s/' % (host, project, rel_path)
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001643
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001644 # Perform all the desired presubmit checks.
1645 results = []
Ben Pastene8351dc12020-08-06 05:01:35 +00001646
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001647 try:
1648 version = [
1649 int(x)
1650 for x in context.get('PRESUBMIT_VERSION', '0.0.0').split('.')
1651 ]
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001652
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001653 with rdb_wrapper.client(prefix) as sink:
1654 if version >= [2, 0, 0]:
1655 # Copy the keys to prevent "dictionary changed size during
1656 # iteration" exception if checks add globals to context.
1657 # E.g. sometimes the Python runtime will add
1658 # __warningregistry__.
1659 for function_name in list(context.keys()):
1660 if not function_name.startswith('Check'):
1661 continue
1662 if function_name.endswith(
1663 'Commit') and not self.committing:
1664 continue
1665 if function_name.endswith('Upload') and self.committing:
1666 continue
1667 logging.debug('Running %s in %s', function_name,
1668 presubmit_path)
1669 results.extend(
1670 self._run_check_function(function_name, context,
1671 sink, presubmit_path))
1672 logging.debug('Running %s done.', function_name)
1673 self.more_cc.extend(output_api.more_cc)
1674 # Clear the CC list between running each presubmit check
1675 # to prevent CCs from being repeatedly appended.
1676 output_api.more_cc = []
Scott Leecc2fe9b2020-11-19 19:38:06 +00001677
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001678 else: # Old format
1679 if self.committing:
1680 function_name = 'CheckChangeOnCommit'
1681 else:
1682 function_name = 'CheckChangeOnUpload'
1683 if function_name in list(context.keys()):
1684 logging.debug('Running %s in %s', function_name,
1685 presubmit_path)
1686 results.extend(
1687 self._run_check_function(function_name, context,
1688 sink, presubmit_path))
1689 logging.debug('Running %s done.', function_name)
1690 self.more_cc.extend(output_api.more_cc)
1691 # Clear the CC list between running each presubmit check
1692 # to prevent CCs from being repeatedly appended.
1693 output_api.more_cc = []
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001694
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001695 finally:
1696 for f in input_api._named_temporary_files:
1697 os.remove(f)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001698
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001699 self.more_cc = sorted(set(self.more_cc))
Daniel Cheng541638f2023-05-15 22:00:47 +00001700
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001701 return results
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001702
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001703 def _run_check_function(self, function_name, context, sink, presubmit_path):
1704 """Evaluates and returns the result of a given presubmit function.
Scott Leecc2fe9b2020-11-19 19:38:06 +00001705
1706 If sink is given, the result of the presubmit function will be reported
1707 to the ResultSink.
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001708
1709 Args:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001710 function_name: the name of the presubmit function to evaluate
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001711 context: a context dictionary in which the function will be evaluated
Scott Leecc2fe9b2020-11-19 19:38:06 +00001712 sink: an instance of ResultSink. None, by default.
1713 Returns:
1714 the result of the presubmit function call.
1715 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001716 start_time = time_time()
1717 try:
1718 result = eval(function_name + '(*__args)', context)
1719 self._check_result_type(result)
1720 except Exception:
1721 _, e_value, _ = sys.exc_info()
1722 result = [
1723 OutputApi.PresubmitError(
1724 'Evaluation of %s failed: %s, %s' %
1725 (function_name, e_value, traceback.format_exc()))
1726 ]
Scott Leecc2fe9b2020-11-19 19:38:06 +00001727
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001728 elapsed_time = time_time() - start_time
1729 if elapsed_time > 10.0:
1730 sys.stdout.write('%6.1fs to run %s from %s.\n' %
1731 (elapsed_time, function_name, presubmit_path))
1732 if sink:
1733 failure_reason = None
1734 status = rdb_wrapper.STATUS_PASS
1735 if any(r.fatal for r in result):
1736 status = rdb_wrapper.STATUS_FAIL
1737 failure_reasons = []
1738 for r in result:
1739 fields = r.json_format()
1740 message = fields['message']
1741 items = '\n'.join(' %s' % item for item in fields['items'])
1742 failure_reasons.append('\n'.join([message, items]))
1743 if failure_reasons:
1744 failure_reason = '\n'.join(failure_reasons)
1745 sink.report(function_name, status, elapsed_time, failure_reason)
Scott Leecc2fe9b2020-11-19 19:38:06 +00001746
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001747 return result
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001748
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001749 def _check_result_type(self, result):
1750 """Helper function which ensures result is a list, and all elements are
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001751 instances of OutputApi.PresubmitResult"""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001752 if not isinstance(result, (tuple, list)):
1753 raise PresubmitFailure(
1754 'Presubmit functions must return a tuple or list')
1755 if not all(
1756 isinstance(res, OutputApi.PresubmitResult) for res in result):
1757 raise PresubmitFailure(
1758 'All presubmit results must be of types derived from '
1759 'output_api.PresubmitResult')
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001760
1761
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001762def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001763 committing,
1764 verbose,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001765 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001766 may_prompt,
Aaron Gable668c1d82018-04-03 10:19:16 -07001767 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04001768 dry_run=None,
Debrian Figueroadd2737e2019-06-21 23:50:13 +00001769 parallel=False,
Dirk Pranke6f0df682021-06-25 00:42:33 +00001770 json_output=None,
Bruce Dawson09c0c072022-05-26 20:28:58 +00001771 no_diffs=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001772 """Runs all presubmit checks that apply to the files in the change.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001773
1774 This finds all PRESUBMIT.py files in directories enclosing the files in the
1775 change (up to the repository root) and calls the relevant entrypoint function
1776 depending on whether the change is being committed or uploaded.
1777
1778 Prints errors, warnings and notifications. Prompts the user for warnings
1779 when needed.
1780
1781 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001782 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001783 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001784 verbose: Prints debug info.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001785 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001786 may_prompt: Enable (y/n) questions on warning or error. If False,
1787 any questions are answered with yes by default.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001788 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001789 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001790 parallel: if true, all tests specified by input_api.RunTests in all
1791 PRESUBMIT files will be run in parallel.
Bruce Dawson09c0c072022-05-26 20:28:58 +00001792 no_diffs: if true, implies that --files or --all was specified so some
1793 checks can be skipped, and some errors will be messages.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001794 Return:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001795 1 if presubmit checks failed or 0 otherwise.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001796 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001797 old_environ = os.environ
1798 try:
1799 # Make sure python subprocesses won't generate .pyc files.
1800 os.environ = os.environ.copy()
1801 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001802
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001803 python_version = 'Python %s' % sys.version_info.major
1804 if committing:
1805 sys.stdout.write('Running %s presubmit commit checks ...\n' %
1806 python_version)
1807 else:
1808 sys.stdout.write('Running %s presubmit upload checks ...\n' %
1809 python_version)
1810 start_time = time_time()
1811 presubmit_files = ListRelevantPresubmitFiles(
1812 change.AbsoluteLocalPaths(), change.RepositoryRoot())
1813 if not presubmit_files and verbose:
1814 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
1815 results = []
1816 thread_pool = ThreadPool()
1817 executer = PresubmitExecuter(change, committing, verbose, gerrit_obj,
1818 dry_run, thread_pool, parallel, no_diffs)
1819 if default_presubmit:
1820 if verbose:
1821 sys.stdout.write('Running default presubmit script.\n')
1822 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1823 results += executer.ExecPresubmitScript(default_presubmit,
1824 fake_path)
1825 for filename in presubmit_files:
1826 filename = os.path.abspath(filename)
1827 # Accept CRLF presubmit script.
1828 presubmit_script = gclient_utils.FileRead(filename).replace(
1829 '\r\n', '\n')
1830 if verbose:
1831 sys.stdout.write('Running %s\n' % filename)
1832 results += executer.ExecPresubmitScript(presubmit_script, filename)
Bruce Dawson76fe1a72022-05-25 20:52:21 +00001833
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001834 results += thread_pool.RunAsync()
Edward Lesmes8e282792018-04-03 18:50:29 -04001835
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001836 messages = {}
1837 should_prompt = False
1838 presubmits_failed = False
1839 for result in results:
1840 if result.fatal:
1841 presubmits_failed = True
1842 messages.setdefault('ERRORS', []).append(result)
1843 elif result.should_prompt:
1844 should_prompt = True
1845 messages.setdefault('Warnings', []).append(result)
1846 else:
1847 messages.setdefault('Messages', []).append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001848
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001849 # Print the different message types in a consistent order. ERRORS go
1850 # last so that they will be most visible in the local-presubmit output.
1851 for name in ['Messages', 'Warnings', 'ERRORS']:
1852 if name in messages:
1853 items = messages[name]
1854 sys.stdout.write('** Presubmit %s: %d **\n' %
1855 (name, len(items)))
1856 for item in items:
1857 item.handle()
1858 sys.stdout.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001859
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001860 total_time = time_time() - start_time
1861 if total_time > 1.0:
1862 sys.stdout.write('Presubmit checks took %.1fs to calculate.\n' %
1863 total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001864
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001865 if not should_prompt and not presubmits_failed:
1866 sys.stdout.write('%s presubmit checks passed.\n\n' % python_version)
1867 elif should_prompt and not presubmits_failed:
1868 sys.stdout.write('There were %s presubmit warnings. ' %
1869 python_version)
1870 if may_prompt:
1871 presubmits_failed = not prompt_should_continue(
1872 'Are you sure you wish to continue? (y/N): ')
1873 else:
1874 sys.stdout.write('\n')
1875 else:
1876 sys.stdout.write('There were %s presubmit errors.\n' %
1877 python_version)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001878
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001879 if json_output:
1880 # Write the presubmit results to json output
1881 presubmit_results = {
1882 'errors':
1883 [error.json_format() for error in messages.get('ERRORS', [])],
1884 'notifications': [
1885 notification.json_format()
1886 for notification in messages.get('Messages', [])
1887 ],
1888 'warnings': [
1889 warning.json_format()
1890 for warning in messages.get('Warnings', [])
1891 ],
1892 'more_cc':
1893 executer.more_cc,
1894 }
Edward Lemur1dc66e12020-02-21 21:36:34 +00001895
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001896 gclient_utils.FileWrite(
1897 json_output, json.dumps(presubmit_results, sort_keys=True))
Edward Lemur1dc66e12020-02-21 21:36:34 +00001898
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001899 global _ASKED_FOR_FEEDBACK
1900 # Ask for feedback one time out of 5.
1901 if (results and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1902 sys.stdout.write(
1903 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1904 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1905 'on the file to figure out who to ask for help.\n')
1906 _ASKED_FOR_FEEDBACK = True
Edward Lemur6eb1d322020-02-27 22:20:15 +00001907
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001908 return 1 if presubmits_failed else 0
1909 finally:
1910 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001911
1912
Edward Lemur50984a62020-02-06 18:10:18 +00001913def _scan_sub_dirs(mask, recursive):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001914 if not recursive:
1915 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001916
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001917 results = []
1918 for root, dirs, files in os.walk('.'):
1919 if '.svn' in dirs:
1920 dirs.remove('.svn')
1921 if '.git' in dirs:
1922 dirs.remove('.git')
1923 for name in files:
1924 if fnmatch.fnmatch(name, mask):
1925 results.append(os.path.join(root, name))
1926 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001927
1928
Edward Lemur50984a62020-02-06 18:10:18 +00001929def _parse_files(args, recursive):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001930 logging.debug('Searching for %s', args)
1931 files = []
1932 for arg in args:
1933 files.extend([('M', f) for f in _scan_sub_dirs(arg, recursive)])
1934 return files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001935
1936
Edward Lemur50984a62020-02-06 18:10:18 +00001937def _parse_change(parser, options):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001938 """Process change options.
Edward Lemur50984a62020-02-06 18:10:18 +00001939
1940 Args:
1941 parser: The parser used to parse the arguments from command line.
1942 options: The arguments parsed from command line.
1943 Returns:
Josip Sokcevic7958e302023-03-01 23:02:21 +00001944 A GitChange if the change root is a git repository, or a Change otherwise.
Edward Lemur50984a62020-02-06 18:10:18 +00001945 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001946 if options.files and options.all_files:
1947 parser.error('<files> cannot be specified when --all-files is set.')
Edward Lemur50984a62020-02-06 18:10:18 +00001948
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001949 change_scm = scm.determine_scm(options.root)
1950 if change_scm != 'git' and not options.files:
1951 parser.error('<files> is not optional for unversioned directories.')
Edward Lemur50984a62020-02-06 18:10:18 +00001952
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001953 if options.files:
1954 if options.source_controlled_only:
1955 # Get the filtered set of files from SCM.
1956 change_files = []
1957 for name in scm.GIT.GetAllFiles(options.root):
1958 for mask in options.files:
1959 if fnmatch.fnmatch(name, mask):
1960 change_files.append(('M', name))
1961 break
1962 else:
1963 # Get the filtered set of files from a directory scan.
1964 change_files = _parse_files(options.files, options.recursive)
1965 elif options.all_files:
1966 change_files = [('M', f) for f in scm.GIT.GetAllFiles(options.root)]
Josip Sokcevic017544d2022-03-31 23:47:53 +00001967 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001968 change_files = scm.GIT.CaptureStatus(options.root, options.upstream
1969 or None)
Edward Lemur50984a62020-02-06 18:10:18 +00001970
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001971 logging.info('Found %d file(s).', len(change_files))
Edward Lemur50984a62020-02-06 18:10:18 +00001972
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001973 change_class = GitChange if change_scm == 'git' else Change
1974 return change_class(options.name,
1975 options.description,
1976 options.root,
1977 change_files,
1978 options.issue,
1979 options.patchset,
1980 options.author,
1981 upstream=options.upstream)
Edward Lemur50984a62020-02-06 18:10:18 +00001982
1983
1984def _parse_gerrit_options(parser, options):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001985 """Process gerrit options.
Edward Lemur50984a62020-02-06 18:10:18 +00001986
1987 SIDE EFFECTS: Modifies options.author and options.description from Gerrit if
1988 options.gerrit_fetch is set.
1989
1990 Args:
1991 parser: The parser used to parse the arguments from command line.
1992 options: The arguments parsed from command line.
1993 Returns:
Stephen Martinisfb09de22021-02-25 03:41:13 +00001994 A GerritAccessor object if options.gerrit_url is set, or None otherwise.
Edward Lemur50984a62020-02-06 18:10:18 +00001995 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001996 gerrit_obj = None
1997 if options.gerrit_url:
1998 gerrit_obj = GerritAccessor(url=options.gerrit_url,
1999 project=options.gerrit_project,
2000 branch=options.gerrit_branch)
Edward Lemur50984a62020-02-06 18:10:18 +00002001
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002002 if not options.gerrit_fetch:
2003 return gerrit_obj
2004
2005 if not options.gerrit_url or not options.issue or not options.patchset:
2006 parser.error(
2007 '--gerrit_fetch requires --gerrit_url, --issue and --patchset.')
2008
2009 options.author = gerrit_obj.GetChangeOwner(options.issue)
2010 options.description = gerrit_obj.GetChangeDescription(
2011 options.issue, options.patchset)
2012
2013 logging.info('Got author: "%s"', options.author)
2014 logging.info('Got description: """\n%s\n"""', options.description)
2015
Edward Lemur50984a62020-02-06 18:10:18 +00002016 return gerrit_obj
2017
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00002018
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002019@contextlib.contextmanager
2020def canned_check_filter(method_names):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002021 filtered = {}
2022 try:
2023 for method_name in method_names:
2024 if not hasattr(presubmit_canned_checks, method_name):
2025 logging.warning('Skipping unknown "canned" check %s' %
2026 method_name)
2027 continue
2028 filtered[method_name] = getattr(presubmit_canned_checks,
2029 method_name)
2030 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
2031 yield
2032 finally:
2033 for name, method in filtered.items():
2034 setattr(presubmit_canned_checks, name, method)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002035
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00002036
sbc@chromium.org013731e2015-02-26 18:28:43 +00002037def main(argv=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002038 parser = argparse.ArgumentParser(usage='%(prog)s [options] <files...>')
2039 hooks = parser.add_mutually_exclusive_group()
2040 hooks.add_argument('-c',
2041 '--commit',
2042 action='store_true',
2043 help='Use commit instead of upload checks.')
2044 hooks.add_argument('-u',
2045 '--upload',
2046 action='store_false',
2047 dest='commit',
2048 help='Use upload instead of commit checks.')
2049 hooks.add_argument('--post_upload',
2050 action='store_true',
2051 help='Run post-upload commit hooks.')
2052 parser.add_argument('-r',
2053 '--recursive',
2054 action='store_true',
2055 help='Act recursively.')
2056 parser.add_argument('-v',
2057 '--verbose',
2058 action='count',
2059 default=0,
2060 help='Use 2 times for more debug info.')
2061 parser.add_argument('--name', default='no name')
2062 parser.add_argument('--author')
2063 desc = parser.add_mutually_exclusive_group()
2064 desc.add_argument('--description',
2065 default='',
2066 help='The change description.')
2067 desc.add_argument('--description_file',
2068 help='File to read change description from.')
2069 parser.add_argument('--issue', type=int, default=0)
2070 parser.add_argument('--patchset', type=int, default=0)
2071 parser.add_argument('--root',
2072 default=os.getcwd(),
2073 help='Search for PRESUBMIT.py up to this directory. '
2074 'If inherit-review-settings-ok is present in this '
2075 'directory, parent directories up to the root file '
2076 'system directories will also be searched.')
2077 parser.add_argument(
2078 '--upstream',
2079 help='Git only: the base ref or upstream branch against '
2080 'which the diff should be computed.')
2081 parser.add_argument('--default_presubmit')
2082 parser.add_argument('--may_prompt', action='store_true', default=False)
2083 parser.add_argument(
2084 '--skip_canned',
2085 action='append',
2086 default=[],
2087 help='A list of checks to skip which appear in '
2088 'presubmit_canned_checks. Can be provided multiple times '
2089 'to skip multiple canned checks.')
2090 parser.add_argument('--dry_run',
2091 action='store_true',
2092 help=argparse.SUPPRESS)
2093 parser.add_argument('--gerrit_url', help=argparse.SUPPRESS)
2094 parser.add_argument('--gerrit_project', help=argparse.SUPPRESS)
2095 parser.add_argument('--gerrit_branch', help=argparse.SUPPRESS)
2096 parser.add_argument('--gerrit_fetch',
2097 action='store_true',
2098 help=argparse.SUPPRESS)
2099 parser.add_argument('--parallel',
2100 action='store_true',
2101 help='Run all tests specified by input_api.RunTests in '
2102 'all PRESUBMIT files in parallel.')
2103 parser.add_argument('--json_output',
2104 help='Write presubmit errors to json output.')
2105 parser.add_argument('--all_files',
2106 action='store_true',
2107 help='Mark all files under source control as modified.')
Bruce Dawson09c0c072022-05-26 20:28:58 +00002108
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002109 parser.add_argument('files',
2110 nargs='*',
2111 help='List of files to be marked as modified when '
2112 'executing presubmit or post-upload hooks. fnmatch '
2113 'wildcards can also be used.')
2114 parser.add_argument('--source_controlled_only',
2115 action='store_true',
2116 help='Constrain \'files\' to those in source control.')
2117 parser.add_argument('--no_diffs',
2118 action='store_true',
2119 help='Assume that all "modified" files have no diffs.')
2120 options = parser.parse_args(argv)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00002121
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002122 log_level = logging.ERROR
2123 if options.verbose >= 2:
2124 log_level = logging.DEBUG
2125 elif options.verbose:
2126 log_level = logging.INFO
2127 log_format = ('[%(levelname).1s%(asctime)s %(process)d %(thread)d '
2128 '%(filename)s] %(message)s')
2129 logging.basicConfig(format=log_format, level=log_level)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00002130
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002131 # Print call stacks when _PresubmitResult objects are created with -v -v is
2132 # specified. This helps track down where presubmit messages are coming from.
2133 if options.verbose >= 2:
2134 global _SHOW_CALLSTACKS
2135 _SHOW_CALLSTACKS = True
Bruce Dawsondca14bc2022-09-15 20:59:38 +00002136
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002137 if options.description_file:
2138 options.description = gclient_utils.FileRead(options.description_file)
2139 gerrit_obj = _parse_gerrit_options(parser, options)
2140 change = _parse_change(parser, options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00002141
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002142 try:
2143 if options.post_upload:
2144 return DoPostUploadExecuter(change, gerrit_obj, options.verbose)
2145 with canned_check_filter(options.skip_canned):
2146 return DoPresubmitChecks(change, options.commit, options.verbose,
2147 options.default_presubmit,
2148 options.may_prompt, gerrit_obj,
2149 options.dry_run, options.parallel,
2150 options.json_output, options.no_diffs)
2151 except PresubmitFailure as e:
2152 import utils
2153 print(e, file=sys.stderr)
2154 print('Maybe your depot_tools is out of date?', file=sys.stderr)
2155 print('depot_tools version: %s' % utils.depot_tools_version(),
2156 file=sys.stderr)
2157 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002158
2159
2160if __name__ == '__main__':
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002161 fix_encoding.fix_encoding()
2162 try:
2163 sys.exit(main())
2164 except KeyboardInterrupt:
2165 sys.stderr.write('interrupted\n')
2166 sys.exit(2)