blob: 81b8fecec3b95df3855b8aad03cd5a2a13aed1b7 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
maruel@chromium.org3bbf2942012-01-10 16:52:06 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Enables directory-specific presubmit checks to run at upload and/or commit.
7"""
8
Raul Tambre80ee78e2019-05-06 22:41:05 +00009from __future__ import print_function
10
Saagar Sanghavi99816902020-08-11 22:41:25 +000011__version__ = '2.0.0'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000012
13# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
14# caching (between all different invocations of presubmit scripts for a given
15# change). We should add it as our presubmit scripts start feeling slow.
16
Edward Lemura5799e32020-01-17 19:26:51 +000017import argparse
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +090018import ast # Exposed through the API.
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +000019import contextlib
Yoshisato Yanagisawa406de132018-06-29 05:43:25 +000020import cpplint
dcheng091b7db2016-06-16 01:27:51 -070021import fnmatch # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000022import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000023import inspect
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +000024import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000025import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000026import logging
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000027import multiprocessing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000028import os # Somewhat exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000029import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000030import re # Exposed through the API.
Edward Lesmes8e282792018-04-03 18:50:29 -040031import signal
Allen Webbfe7d7092021-05-18 02:05:49 +000032import six
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000033import sys # Parts exposed through API.
34import tempfile # Exposed through the API.
Edward Lesmes8e282792018-04-03 18:50:29 -040035import threading
jam@chromium.org2a891dc2009-08-20 20:33:37 +000036import time
Edward Lemurde9e3ca2019-10-24 21:13:31 +000037import traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +000038import unittest # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000039from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000040
41# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000042import fix_encoding
Yoshisato Yanagisawa04600b42019-03-15 03:03:41 +000043import gclient_paths # Exposed through the API
44import gclient_utils
Aaron Gableb584c4f2017-04-26 16:28:08 -070045import git_footers
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000046import gerrit_util
Edward Lesmes9ce03f82021-01-12 20:13:31 +000047import owners as owners_db
48import owners_client
Jochen Eisinger76f5fc62017-04-07 16:27:46 +020049import owners_finder
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050import presubmit_canned_checks
Saagar Sanghavi9949ab72020-07-20 20:56:40 +000051import rdb_wrapper
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000052import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000053import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000054
Edward Lemur16af3562019-10-17 22:11:33 +000055if sys.version_info.major == 2:
56 # TODO(1009814): Expose urllib2 only through urllib_request and urllib_error
57 import urllib2 # Exposed through the API.
58 import urlparse
59 import urllib2 as urllib_request
60 import urllib2 as urllib_error
61else:
62 import urllib.parse as urlparse
63 import urllib.request as urllib_request
64 import urllib.error as urllib_error
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000065
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000066# Ask for feedback only once in program lifetime.
67_ASKED_FOR_FEEDBACK = False
68
Edward Lemurecc27072020-01-06 16:42:34 +000069def time_time():
70 # Use this so that it can be mocked in tests without interfering with python
71 # system machinery.
72 return time.time()
73
74
maruel@chromium.org899e1c12011-04-07 17:03:18 +000075class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000076 pass
77
78
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000079class CommandData(object):
Edward Lemur940c2822019-08-23 00:34:25 +000080 def __init__(self, name, cmd, kwargs, message, python3=False):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000081 self.name = name
82 self.cmd = cmd
Edward Lesmes8e282792018-04-03 18:50:29 -040083 self.stdin = kwargs.get('stdin', None)
Edward Lemur2d6b67c2019-08-23 22:25:41 +000084 self.kwargs = kwargs.copy()
Edward Lesmes8e282792018-04-03 18:50:29 -040085 self.kwargs['stdout'] = subprocess.PIPE
86 self.kwargs['stderr'] = subprocess.STDOUT
87 self.kwargs['stdin'] = subprocess.PIPE
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000088 self.message = message
89 self.info = None
Edward Lemur940c2822019-08-23 00:34:25 +000090 self.python3 = python3
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000091
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000092
Edward Lesmes8e282792018-04-03 18:50:29 -040093# Adapted from
94# https://github.com/google/gtest-parallel/blob/master/gtest_parallel.py#L37
95#
96# An object that catches SIGINT sent to the Python process and notices
97# if processes passed to wait() die by SIGINT (we need to look for
98# both of those cases, because pressing Ctrl+C can result in either
99# the main process or one of the subprocesses getting the signal).
100#
101# Before a SIGINT is seen, wait(p) will simply call p.wait() and
102# return the result. Once a SIGINT has been seen (in the main process
103# or a subprocess, including the one the current call is waiting for),
Edward Lemur9a5bb612019-09-26 02:01:52 +0000104# wait(p) will call p.terminate().
Edward Lesmes8e282792018-04-03 18:50:29 -0400105class SigintHandler(object):
Edward Lesmes8e282792018-04-03 18:50:29 -0400106 sigint_returncodes = {-signal.SIGINT, # Unix
107 -1073741510, # Windows
108 }
109 def __init__(self):
110 self.__lock = threading.Lock()
111 self.__processes = set()
112 self.__got_sigint = False
Edward Lemur9a5bb612019-09-26 02:01:52 +0000113 self.__previous_signal = signal.signal(signal.SIGINT, self.interrupt)
Edward Lesmes8e282792018-04-03 18:50:29 -0400114
115 def __on_sigint(self):
116 self.__got_sigint = True
117 while self.__processes:
118 try:
119 self.__processes.pop().terminate()
120 except OSError:
121 pass
122
Edward Lemur9a5bb612019-09-26 02:01:52 +0000123 def interrupt(self, signal_num, frame):
Edward Lesmes8e282792018-04-03 18:50:29 -0400124 with self.__lock:
125 self.__on_sigint()
Edward Lemur9a5bb612019-09-26 02:01:52 +0000126 self.__previous_signal(signal_num, frame)
Edward Lesmes8e282792018-04-03 18:50:29 -0400127
128 def got_sigint(self):
129 with self.__lock:
130 return self.__got_sigint
131
132 def wait(self, p, stdin):
133 with self.__lock:
134 if self.__got_sigint:
135 p.terminate()
136 self.__processes.add(p)
137 stdout, stderr = p.communicate(stdin)
138 code = p.returncode
139 with self.__lock:
140 self.__processes.discard(p)
141 if code in self.sigint_returncodes:
142 self.__on_sigint()
Edward Lesmes8e282792018-04-03 18:50:29 -0400143 return stdout, stderr
144
145sigint_handler = SigintHandler()
146
147
Edward Lemurecc27072020-01-06 16:42:34 +0000148class Timer(object):
149 def __init__(self, timeout, fn):
150 self.completed = False
151 self._fn = fn
152 self._timer = threading.Timer(timeout, self._onTimer) if timeout else None
153
154 def __enter__(self):
155 if self._timer:
156 self._timer.start()
157 return self
158
159 def __exit__(self, _type, _value, _traceback):
160 if self._timer:
161 self._timer.cancel()
162
163 def _onTimer(self):
164 self._fn()
165 self.completed = True
166
167
Edward Lesmes8e282792018-04-03 18:50:29 -0400168class ThreadPool(object):
Edward Lemurecc27072020-01-06 16:42:34 +0000169 def __init__(self, pool_size=None, timeout=None):
170 self.timeout = timeout
Edward Lesmes8e282792018-04-03 18:50:29 -0400171 self._pool_size = pool_size or multiprocessing.cpu_count()
172 self._messages = []
173 self._messages_lock = threading.Lock()
174 self._tests = []
175 self._tests_lock = threading.Lock()
176 self._nonparallel_tests = []
177
Edward Lemurecc27072020-01-06 16:42:34 +0000178 def _GetCommand(self, test):
Edward Lemur940c2822019-08-23 00:34:25 +0000179 vpython = 'vpython'
180 if test.python3:
181 vpython += '3'
182 if sys.platform == 'win32':
183 vpython += '.bat'
Edward Lesmes8e282792018-04-03 18:50:29 -0400184
185 cmd = test.cmd
186 if cmd[0] == 'python':
187 cmd = list(cmd)
188 cmd[0] = vpython
189 elif cmd[0].endswith('.py'):
190 cmd = [vpython] + cmd
191
Edward Lemur336e51f2019-11-14 21:42:04 +0000192 # On Windows, scripts on the current directory take precedence over PATH, so
193 # that when testing depot_tools on Windows, calling `vpython.bat` will
194 # execute the copy of vpython of the depot_tools under test instead of the
195 # one in the bot.
196 # As a workaround, we run the tests from the parent directory instead.
197 if (cmd[0] == vpython and
198 'cwd' in test.kwargs and
199 os.path.basename(test.kwargs['cwd']) == 'depot_tools'):
200 test.kwargs['cwd'] = os.path.dirname(test.kwargs['cwd'])
201 cmd[1] = os.path.join('depot_tools', cmd[1])
202
Edward Lemurecc27072020-01-06 16:42:34 +0000203 return cmd
204
205 def _RunWithTimeout(self, cmd, stdin, kwargs):
206 p = subprocess.Popen(cmd, **kwargs)
207 with Timer(self.timeout, p.terminate) as timer:
208 stdout, _ = sigint_handler.wait(p, stdin)
Josip Sokcevic6635baf2021-11-19 02:04:39 +0000209 stdout = stdout.decode('utf-8', 'ignore')
Edward Lemurecc27072020-01-06 16:42:34 +0000210 if timer.completed:
211 stdout = 'Process timed out after %ss\n%s' % (self.timeout, stdout)
Josip Sokcevic6635baf2021-11-19 02:04:39 +0000212 return p.returncode, stdout
Edward Lemurecc27072020-01-06 16:42:34 +0000213
214 def CallCommand(self, test):
215 """Runs an external program.
216
Edward Lemura5799e32020-01-17 19:26:51 +0000217 This function converts invocation of .py files and invocations of 'python'
Edward Lemurecc27072020-01-06 16:42:34 +0000218 to vpython invocations.
219 """
220 cmd = self._GetCommand(test)
Edward Lesmes8e282792018-04-03 18:50:29 -0400221 try:
Edward Lemurecc27072020-01-06 16:42:34 +0000222 start = time_time()
223 returncode, stdout = self._RunWithTimeout(cmd, test.stdin, test.kwargs)
224 duration = time_time() - start
Edward Lemur7cf94382019-11-15 22:36:41 +0000225 except Exception:
Edward Lemurecc27072020-01-06 16:42:34 +0000226 duration = time_time() - start
Edward Lesmes8e282792018-04-03 18:50:29 -0400227 return test.message(
Edward Lemur7cf94382019-11-15 22:36:41 +0000228 '%s\n%s exec failure (%4.2fs)\n%s' % (
229 test.name, ' '.join(cmd), duration, traceback.format_exc()))
Edward Lemurde9e3ca2019-10-24 21:13:31 +0000230
Edward Lemurecc27072020-01-06 16:42:34 +0000231 if returncode != 0:
Edward Lesmes8e282792018-04-03 18:50:29 -0400232 return test.message(
Edward Lemur7cf94382019-11-15 22:36:41 +0000233 '%s\n%s (%4.2fs) failed\n%s' % (
234 test.name, ' '.join(cmd), duration, stdout))
Edward Lemurecc27072020-01-06 16:42:34 +0000235
Edward Lesmes8e282792018-04-03 18:50:29 -0400236 if test.info:
Edward Lemur7cf94382019-11-15 22:36:41 +0000237 return test.info('%s\n%s (%4.2fs)' % (test.name, ' '.join(cmd), duration))
Edward Lesmes8e282792018-04-03 18:50:29 -0400238
239 def AddTests(self, tests, parallel=True):
240 if parallel:
241 self._tests.extend(tests)
242 else:
243 self._nonparallel_tests.extend(tests)
244
245 def RunAsync(self):
246 self._messages = []
247
248 def _WorkerFn():
249 while True:
250 test = None
251 with self._tests_lock:
252 if not self._tests:
253 break
254 test = self._tests.pop()
255 result = self.CallCommand(test)
256 if result:
257 with self._messages_lock:
258 self._messages.append(result)
259
260 def _StartDaemon():
261 t = threading.Thread(target=_WorkerFn)
262 t.daemon = True
263 t.start()
264 return t
265
266 while self._nonparallel_tests:
267 test = self._nonparallel_tests.pop()
268 result = self.CallCommand(test)
269 if result:
270 self._messages.append(result)
271
272 if self._tests:
273 threads = [_StartDaemon() for _ in range(self._pool_size)]
274 for worker in threads:
275 worker.join()
276
277 return self._messages
278
279
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000280def normpath(path):
281 '''Version of os.path.normpath that also changes backward slashes to
282 forward slashes when not running on Windows.
283 '''
284 # This is safe to always do because the Windows version of os.path.normpath
285 # will replace forward slashes with backward slashes.
286 path = path.replace(os.sep, '/')
287 return os.path.normpath(path)
288
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000289
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000290def _RightHandSideLinesImpl(affected_files):
291 """Implements RightHandSideLines for InputApi and GclChange."""
292 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000293 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000294 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000295 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000296
297
Edward Lemur6eb1d322020-02-27 22:20:15 +0000298def prompt_should_continue(prompt_string):
299 sys.stdout.write(prompt_string)
Dirk Pranke7288f882021-06-03 18:01:30 +0000300 sys.stdout.flush()
Edward Lemur6eb1d322020-02-27 22:20:15 +0000301 response = sys.stdin.readline().strip().lower()
302 return response in ('y', 'yes')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000303
304
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000305# Top level object so multiprocessing can pickle
306# Public access through OutputApi object.
307class _PresubmitResult(object):
308 """Base class for result objects."""
309 fatal = False
310 should_prompt = False
311
312 def __init__(self, message, items=None, long_text=''):
313 """
314 message: A short one-line message to indicate errors.
315 items: A list of short strings to indicate where errors occurred.
316 long_text: multi-line text output, e.g. from another tool
317 """
318 self._message = message
319 self._items = items or []
Tom McKee61c72652021-07-20 11:56:32 +0000320 self._long_text = _PresubmitResult._ensure_str(long_text.rstrip())
321
322 @staticmethod
323 def _ensure_str(val):
324 """
325 val: A "stringish" value. Can be any of str, unicode or bytes.
326 returns: A str after applying encoding/decoding as needed.
327 Assumes/uses UTF-8 for relevant inputs/outputs.
328
329 We'd prefer to use six.ensure_str but our copy of six is old :(
330 """
331 if isinstance(val, str):
332 return val
333 if six.PY2 and isinstance(val, unicode):
334 return val.encode()
335 elif six.PY3 and isinstance(val, bytes):
336 return val.decode()
337 raise ValueError("Unknown string type %s" % type(val))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000338
Edward Lemur6eb1d322020-02-27 22:20:15 +0000339 def handle(self):
340 sys.stdout.write(self._message)
341 sys.stdout.write('\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000342 for index, item in enumerate(self._items):
Edward Lemur6eb1d322020-02-27 22:20:15 +0000343 sys.stdout.write(' ')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000344 # Write separately in case it's unicode.
Edward Lemur6eb1d322020-02-27 22:20:15 +0000345 sys.stdout.write(str(item))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000346 if index < len(self._items) - 1:
Edward Lemur6eb1d322020-02-27 22:20:15 +0000347 sys.stdout.write(' \\')
348 sys.stdout.write('\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000349 if self._long_text:
Edward Lemur6eb1d322020-02-27 22:20:15 +0000350 sys.stdout.write('\n***************\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000351 # Write separately in case it's unicode.
Tom McKee61c72652021-07-20 11:56:32 +0000352 sys.stdout.write(self._long_text)
Edward Lemur6eb1d322020-02-27 22:20:15 +0000353 sys.stdout.write('\n***************\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000354
Debrian Figueroadd2737e2019-06-21 23:50:13 +0000355 def json_format(self):
356 return {
357 'message': self._message,
Debrian Figueroa6095d402019-06-28 18:47:18 +0000358 'items': [str(item) for item in self._items],
Debrian Figueroadd2737e2019-06-21 23:50:13 +0000359 'long_text': self._long_text,
360 'fatal': self.fatal
361 }
362
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000363
364# Top level object so multiprocessing can pickle
365# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000366class _PresubmitError(_PresubmitResult):
367 """A hard presubmit error."""
368 fatal = True
369
370
371# Top level object so multiprocessing can pickle
372# Public access through OutputApi object.
373class _PresubmitPromptWarning(_PresubmitResult):
374 """An warning that prompts the user if they want to continue."""
375 should_prompt = True
376
377
378# Top level object so multiprocessing can pickle
379# Public access through OutputApi object.
380class _PresubmitNotifyResult(_PresubmitResult):
381 """Just print something to the screen -- but it's not even a warning."""
382 pass
383
384
385# Top level object so multiprocessing can pickle
386# Public access through OutputApi object.
387class _MailTextResult(_PresubmitResult):
388 """A warning that should be included in the review request email."""
389 def __init__(self, *args, **kwargs):
390 super(_MailTextResult, self).__init__()
391 raise NotImplementedError()
392
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000393class GerritAccessor(object):
394 """Limited Gerrit functionality for canned presubmit checks to work.
395
396 To avoid excessive Gerrit calls, caches the results.
397 """
398
Edward Lesmeseb1bd622021-03-01 19:54:07 +0000399 def __init__(self, url=None, project=None, branch=None):
400 self.host = urlparse.urlparse(url).netloc if url else None
401 self.project = project
402 self.branch = branch
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000403 self.cache = {}
Edward Lesmes8170c292021-03-19 20:04:43 +0000404 self.code_owners_enabled = None
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000405
406 def _FetchChangeDetail(self, issue):
407 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100408 try:
409 return gerrit_util.GetChangeDetail(
410 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700411 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100412 except gerrit_util.GerritError as e:
413 if e.http_status == 404:
414 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
415 'no credentials to fetch issue details' % issue)
416 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000417
418 def GetChangeInfo(self, issue):
419 """Returns labels and all revisions (patchsets) for this issue.
420
421 The result is a dictionary according to Gerrit REST Api.
422 https://gerrit-review.googlesource.com/Documentation/rest-api.html
423
424 However, API isn't very clear what's inside, so see tests for example.
425 """
426 assert issue
427 cache_key = int(issue)
428 if cache_key not in self.cache:
429 self.cache[cache_key] = self._FetchChangeDetail(issue)
430 return self.cache[cache_key]
431
432 def GetChangeDescription(self, issue, patchset=None):
433 """If patchset is none, fetches current patchset."""
434 info = self.GetChangeInfo(issue)
435 # info is a reference to cache. We'll modify it here adding description to
436 # it to the right patchset, if it is not yet there.
437
438 # Find revision info for the patchset we want.
439 if patchset is not None:
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000440 for rev, rev_info in info['revisions'].items():
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000441 if str(rev_info['_number']) == str(patchset):
442 break
443 else:
444 raise Exception('patchset %s doesn\'t exist in issue %s' % (
445 patchset, issue))
446 else:
447 rev = info['current_revision']
448 rev_info = info['revisions'][rev]
449
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100450 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000451
Mun Yong Jang603d01e2017-12-19 16:38:30 -0800452 def GetDestRef(self, issue):
453 ref = self.GetChangeInfo(issue)['branch']
454 if not ref.startswith('refs/'):
455 # NOTE: it is possible to create 'refs/x' branch,
456 # aka 'refs/heads/refs/x'. However, this is ill-advised.
457 ref = 'refs/heads/%s' % ref
458 return ref
459
Edward Lesmes02d4b822020-11-11 00:37:35 +0000460 def _GetApproversForLabel(self, issue, label):
461 change_info = self.GetChangeInfo(issue)
462 label_info = change_info.get('labels', {}).get(label, {})
463 values = label_info.get('values', {}).keys()
464 if not values:
465 return []
466 max_value = max(int(v) for v in values)
467 return [v for v in label_info.get('all', [])
468 if v.get('value', 0) == max_value]
469
Edward Lesmesc4566172021-03-19 16:55:13 +0000470 def IsBotCommitApproved(self, issue):
471 return bool(self._GetApproversForLabel(issue, 'Bot-Commit'))
472
Edward Lesmescf49cb82020-11-11 01:08:36 +0000473 def IsOwnersOverrideApproved(self, issue):
474 return bool(self._GetApproversForLabel(issue, 'Owners-Override'))
475
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000476 def GetChangeOwner(self, issue):
477 return self.GetChangeInfo(issue)['owner']['email']
478
479 def GetChangeReviewers(self, issue, approving_only=True):
Aaron Gable8b478f02017-07-31 15:33:19 -0700480 changeinfo = self.GetChangeInfo(issue)
481 if approving_only:
Edward Lesmes02d4b822020-11-11 00:37:35 +0000482 reviewers = self._GetApproversForLabel(issue, 'Code-Review')
Aaron Gable8b478f02017-07-31 15:33:19 -0700483 else:
484 reviewers = changeinfo.get('reviewers', {}).get('REVIEWER', [])
485 return [r.get('email') for r in reviewers]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000486
Edward Lemure4d329c2020-02-03 20:41:18 +0000487 def UpdateDescription(self, description, issue):
488 gerrit_util.SetCommitMessage(self.host, issue, description, notify='NONE')
489
Edward Lesmes8170c292021-03-19 20:04:43 +0000490 def IsCodeOwnersEnabledOnRepo(self):
491 if self.code_owners_enabled is None:
492 self.code_owners_enabled = gerrit_util.IsCodeOwnersEnabledOnRepo(
Edward Lesmes392c4072021-03-19 21:58:45 +0000493 self.host, self.project)
Edward Lesmes8170c292021-03-19 20:04:43 +0000494 return self.code_owners_enabled
495
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000496
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000497class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000498 """An instance of OutputApi gets passed to presubmit scripts so that they
499 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000500 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000501 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000502 PresubmitError = _PresubmitError
503 PresubmitPromptWarning = _PresubmitPromptWarning
504 PresubmitNotifyResult = _PresubmitNotifyResult
505 MailTextResult = _MailTextResult
506
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000507 def __init__(self, is_committing):
508 self.is_committing = is_committing
Daniel Cheng7227d212017-11-17 08:12:37 -0800509 self.more_cc = []
510
511 def AppendCC(self, cc):
512 """Appends a user to cc for this change."""
513 self.more_cc.append(cc)
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000514
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000515 def PresubmitPromptOrNotify(self, *args, **kwargs):
516 """Warn the user when uploading, but only notify if committing."""
517 if self.is_committing:
518 return self.PresubmitNotifyResult(*args, **kwargs)
519 return self.PresubmitPromptWarning(*args, **kwargs)
520
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000521
522class InputApi(object):
523 """An instance of this object is passed to presubmit scripts so they can
524 know stuff about the change they're looking at.
525 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000526 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800527 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000528
maruel@chromium.org3410d912009-06-09 20:56:16 +0000529 # File extensions that are considered source files from a style guide
530 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000531 #
532 # Files without an extension aren't included in the list. If you want to
local_bot30f774e2020-06-25 18:23:34 +0000533 # filter them as source files, add r'(^|.*?[\\\/])[^.]+$' to the allow list.
local_bot64021412020-07-08 21:05:39 +0000534 # Note that ALL CAPS files are skipped in DEFAULT_FILES_TO_SKIP below.
535 DEFAULT_FILES_TO_CHECK = (
maruel@chromium.org3410d912009-06-09 20:56:16 +0000536 # C++ and friends
Edward Lemura5799e32020-01-17 19:26:51 +0000537 r'.+\.c$', r'.+\.cc$', r'.+\.cpp$', r'.+\.h$', r'.+\.m$', r'.+\.mm$',
538 r'.+\.inl$', r'.+\.asm$', r'.+\.hxx$', r'.+\.hpp$', r'.+\.s$', r'.+\.S$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000539 # Scripts
Edward Lemura5799e32020-01-17 19:26:51 +0000540 r'.+\.js$', r'.+\.py$', r'.+\.sh$', r'.+\.rb$', r'.+\.pl$', r'.+\.pm$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000541 # Other
Edward Lemura5799e32020-01-17 19:26:51 +0000542 r'.+\.java$', r'.+\.mk$', r'.+\.am$', r'.+\.css$', r'.+\.mojom$',
543 r'.+\.fidl$'
maruel@chromium.org3410d912009-06-09 20:56:16 +0000544 )
545
546 # Path regexp that should be excluded from being considered containing source
547 # files. Don't modify this list from a presubmit script!
local_bot64021412020-07-08 21:05:39 +0000548 DEFAULT_FILES_TO_SKIP = (
Edward Lemura5799e32020-01-17 19:26:51 +0000549 r'testing_support[\\\/]google_appengine[\\\/].*',
550 r'.*\bexperimental[\\\/].*',
Kent Tamura179dd1e2018-04-26 15:07:41 +0900551 # Exclude third_party/.* but NOT third_party/{WebKit,blink}
552 # (crbug.com/539768 and crbug.com/836555).
Edward Lemura5799e32020-01-17 19:26:51 +0000553 r'.*\bthird_party[\\\/](?!(WebKit|blink)[\\\/]).*',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000554 # Output directories (just in case)
Edward Lemura5799e32020-01-17 19:26:51 +0000555 r'.*\bDebug[\\\/].*',
556 r'.*\bRelease[\\\/].*',
557 r'.*\bxcodebuild[\\\/].*',
558 r'.*\bout[\\\/].*',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000559 # All caps files like README and LICENCE.
Edward Lemura5799e32020-01-17 19:26:51 +0000560 r'.*\b[A-Z0-9_]{2,}$',
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000561 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
Edward Lemura5799e32020-01-17 19:26:51 +0000562 r'(|.*[\\\/])\.git[\\\/].*',
563 r'(|.*[\\\/])\.svn[\\\/].*',
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000564 # There is no point in processing a patch file.
Edward Lemura5799e32020-01-17 19:26:51 +0000565 r'.+\.diff$',
566 r'.+\.patch$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000567 )
568
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000569 def __init__(self, change, presubmit_path, is_committing,
Edward Lesmes8e282792018-04-03 18:50:29 -0400570 verbose, gerrit_obj, dry_run=None, thread_pool=None, parallel=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000571 """Builds an InputApi object.
572
573 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000574 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000575 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000576 is_committing: True if the change is about to be committed.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000577 gerrit_obj: provides basic Gerrit codereview functionality.
578 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -0400579 parallel: if true, all tests reported via input_api.RunTests for all
580 PRESUBMIT files will be run in parallel.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000581 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000582 # Version number of the presubmit_support script.
583 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000584 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000585 self.is_committing = is_committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000586 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000587 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000588
Edward Lesmes8e282792018-04-03 18:50:29 -0400589 self.parallel = parallel
590 self.thread_pool = thread_pool or ThreadPool()
591
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000592 # We expose various modules and functions as attributes of the input_api
593 # so that presubmit scripts don't have to import them.
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +0900594 self.ast = ast
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000595 self.basename = os.path.basename
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000596 self.cpplint = cpplint
dcheng091b7db2016-06-16 01:27:51 -0700597 self.fnmatch = fnmatch
Yoshisato Yanagisawa04600b42019-03-15 03:03:41 +0000598 self.gclient_paths = gclient_paths
Yoshisato Yanagisawa57dd17b2019-03-22 09:10:29 +0000599 # TODO(yyanagisawa): stop exposing this when python3 become default.
600 # Since python3's tempfile has TemporaryDirectory, we do not need this.
601 self.temporary_directory = gclient_utils.temporary_directory
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000602 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000603 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000604 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000605 self.os_listdir = os.listdir
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000606 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000607 self.os_stat = os.stat
Yoshisato Yanagisawa406de132018-06-29 05:43:25 +0000608 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000609 self.re = re
610 self.subprocess = subprocess
Edward Lemurb9830242019-10-30 22:19:20 +0000611 self.sys = sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000612 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000613 self.time = time
maruel@chromium.org1487d532009-06-06 00:22:57 +0000614 self.unittest = unittest
Edward Lemurb9830242019-10-30 22:19:20 +0000615 if sys.version_info.major == 2:
616 self.urllib2 = urllib2
Edward Lemur16af3562019-10-17 22:11:33 +0000617 self.urllib_request = urllib_request
618 self.urllib_error = urllib_error
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000619
Robert Iannucci50258932018-03-19 10:30:59 -0700620 self.is_windows = sys.platform == 'win32'
621
Edward Lemurb9646622019-10-25 20:57:35 +0000622 # Set python_executable to 'vpython' in order to allow scripts in other
623 # repos (e.g. src.git) to automatically pick up that repo's .vpython file,
624 # instead of inheriting the one in depot_tools.
625 self.python_executable = 'vpython'
Erik Staab69135d12021-05-14 22:31:57 +0000626 # Offer a python 3 executable for use during the migration off of python 2.
627 self.python3_executable = 'vpython3'
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000628 self.environ = os.environ
629
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000630 # InputApi.platform is the platform you're currently running on.
631 self.platform = sys.platform
632
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000633 self.cpu_count = multiprocessing.cpu_count()
634
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000635 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000636 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000637
638 # We carry the canned checks so presubmit scripts can easily use them.
639 self.canned_checks = presubmit_canned_checks
640
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100641 # Temporary files we must manually remove at the end of a run.
642 self._named_temporary_files = []
Jochen Eisinger72606f82017-04-04 10:44:18 +0200643
Edward Lesmesdc11c6b2021-03-19 19:22:05 +0000644 self.owners_client = None
645 if self.gerrit:
646 self.owners_client = owners_client.GetCodeOwnersClient(
647 root=change.RepositoryRoot(),
648 upstream=change.UpstreamBranch(),
649 host=self.gerrit.host,
650 project=self.gerrit.project,
651 branch=self.gerrit.branch)
Edward Lesmes9ce03f82021-01-12 20:13:31 +0000652 self.owners_db = owners_db.Database(
653 change.RepositoryRoot(), fopen=open, os_path=self.os_path)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200654 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000655 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000656 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000657
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000658 # Replace <hash_map> and <hash_set> as headers that need to be included
Edward Lemura5799e32020-01-17 19:26:51 +0000659 # with 'base/containers/hash_tables.h' instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000660 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800661 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000662 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000663 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000664 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
665 for (a, b, header) in cpplint._re_pattern_templates
666 ]
667
Edward Lemurecc27072020-01-06 16:42:34 +0000668 def SetTimeout(self, timeout):
669 self.thread_pool.timeout = timeout
670
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000671 def PresubmitLocalPath(self):
672 """Returns the local path of the presubmit script currently being run.
673
674 This is useful if you don't want to hard-code absolute paths in the
675 presubmit script. For example, It can be used to find another file
676 relative to the PRESUBMIT.py script, so the whole tree can be branched and
677 the presubmit script still works, without editing its content.
678 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000679 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680
agable0b65e732016-11-22 09:25:46 -0800681 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000682 """Same as input_api.change.AffectedFiles() except only lists files
683 (and optionally directories) in the same directory as the current presubmit
Bruce Dawson7a0b07a2020-04-23 17:14:40 +0000684 script, or subdirectories thereof. Note that files are listed using the OS
685 path separator, so backslashes are used as separators on Windows.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000686 """
Edward Lemura5799e32020-01-17 19:26:51 +0000687 dir_with_slash = normpath('%s/' % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000688 if len(dir_with_slash) == 1:
689 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000690
Edward Lemurb9830242019-10-30 22:19:20 +0000691 return list(filter(
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000692 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
Edward Lemurb9830242019-10-30 22:19:20 +0000693 self.change.AffectedFiles(include_deletes, file_filter)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000694
agable0b65e732016-11-22 09:25:46 -0800695 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000696 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800697 paths = [af.LocalPath() for af in self.AffectedFiles()]
Edward Lemura5799e32020-01-17 19:26:51 +0000698 logging.debug('LocalPaths: %s', paths)
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000699 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000700
agable0b65e732016-11-22 09:25:46 -0800701 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000702 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800703 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000704
John Budorick16162372018-04-18 10:39:53 -0700705 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
agable0b65e732016-11-22 09:25:46 -0800706 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000707 in the same directory as the current presubmit script, or subdirectories
708 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000710 if include_deletes is not None:
Edward Lemura5799e32020-01-17 19:26:51 +0000711 warn('AffectedTestableFiles(include_deletes=%s)'
712 ' is deprecated and ignored' % str(include_deletes),
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000713 category=DeprecationWarning,
714 stacklevel=2)
Edward Lemurb9830242019-10-30 22:19:20 +0000715 return list(filter(
716 lambda x: x.IsTestableFile(),
717 self.AffectedFiles(include_deletes=False, **kwargs)))
agable0b65e732016-11-22 09:25:46 -0800718
719 def AffectedTextFiles(self, include_deletes=None):
720 """An alias to AffectedTestableFiles for backwards compatibility."""
721 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000722
Josip Sokcevic8c955952021-02-01 21:32:57 +0000723 def FilterSourceFile(self,
724 affected_file,
725 files_to_check=None,
726 files_to_skip=None,
727 allow_list=None,
728 block_list=None):
Edward Lemura5799e32020-01-17 19:26:51 +0000729 """Filters out files that aren't considered 'source file'.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000730
local_bot64021412020-07-08 21:05:39 +0000731 If files_to_check or files_to_skip is None, InputApi.DEFAULT_FILES_TO_CHECK
732 and InputApi.DEFAULT_FILES_TO_SKIP is used respectively.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000733
734 The lists will be compiled as regular expression and
735 AffectedFile.LocalPath() needs to pass both list.
736
737 Note: Copy-paste this function to suit your needs or use a lambda function.
738 """
local_bot64021412020-07-08 21:05:39 +0000739 if files_to_check is None:
740 files_to_check = self.DEFAULT_FILES_TO_CHECK
741 if files_to_skip is None:
742 files_to_skip = self.DEFAULT_FILES_TO_SKIP
local_bot30f774e2020-06-25 18:23:34 +0000743
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000744 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000745 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000746 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000747 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000748 return True
749 return False
local_bot64021412020-07-08 21:05:39 +0000750 return (Find(affected_file, files_to_check) and
751 not Find(affected_file, files_to_skip))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000752
753 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800754 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000755
756 If source_file is None, InputApi.FilterSourceFile() is used.
757 """
758 if not source_file:
759 source_file = self.FilterSourceFile
Edward Lemurb9830242019-10-30 22:19:20 +0000760 return list(filter(source_file, self.AffectedTestableFiles()))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000761
762 def RightHandSideLines(self, source_file_filter=None):
Edward Lemura5799e32020-01-17 19:26:51 +0000763 """An iterator over all text lines in 'new' version of changed files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000764
765 Only lists lines from new or modified text files in the change that are
766 contained by the directory of the currently executing presubmit script.
767
768 This is useful for doing line-by-line regex checks, like checking for
769 trailing whitespace.
770
771 Yields:
772 a 3 tuple:
773 the AffectedFile instance of the current file;
774 integer line number (1-based); and
775 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000776
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000777 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000778 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000779 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000780 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000781
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000782 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000783 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000784
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000785 Deny reading anything outside the repository.
786 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000787 if isinstance(file_item, AffectedFile):
788 file_item = file_item.AbsoluteLocalPath()
789 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000790 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000791 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000792
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100793 def CreateTemporaryFile(self, **kwargs):
794 """Returns a named temporary file that must be removed with a call to
795 RemoveTemporaryFiles().
796
797 All keyword arguments are forwarded to tempfile.NamedTemporaryFile(),
798 except for |delete|, which is always set to False.
799
800 Presubmit checks that need to create a temporary file and pass it for
801 reading should use this function instead of NamedTemporaryFile(), as
802 Windows fails to open a file that is already open for writing.
803
804 with input_api.CreateTemporaryFile() as f:
805 f.write('xyz')
806 f.close()
807 input_api.subprocess.check_output(['script-that', '--reads-from',
808 f.name])
809
810
811 Note that callers of CreateTemporaryFile() should not worry about removing
812 any temporary file; this is done transparently by the presubmit handling
813 code.
814 """
815 if 'delete' in kwargs:
816 # Prevent users from passing |delete|; we take care of file deletion
817 # ourselves and this prevents unintuitive error messages when we pass
818 # delete=False and 'delete' is also in kwargs.
819 raise TypeError('CreateTemporaryFile() does not take a "delete" '
820 'argument, file deletion is handled automatically by '
821 'the same presubmit_support code that creates InputApi '
822 'objects.')
823 temp_file = self.tempfile.NamedTemporaryFile(delete=False, **kwargs)
824 self._named_temporary_files.append(temp_file.name)
825 return temp_file
826
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000827 @property
828 def tbr(self):
829 """Returns if a change is TBR'ed."""
Jeremy Romandce22502017-06-20 15:37:29 -0400830 return 'TBR' in self.change.tags or self.change.TBRsFromDescription()
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000831
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000832 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000833 tests = []
834 msgs = []
835 for t in tests_mix:
Edward Lesmes8e282792018-04-03 18:50:29 -0400836 if isinstance(t, OutputApi.PresubmitResult) and t:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000837 msgs.append(t)
838 else:
839 assert issubclass(t.message, _PresubmitResult)
840 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000841 if self.verbose:
842 t.info = _PresubmitNotifyResult
Edward Lemur1037c742018-05-01 18:56:04 -0400843 if not t.kwargs.get('cwd'):
844 t.kwargs['cwd'] = self.PresubmitLocalPath()
Edward Lesmes8e282792018-04-03 18:50:29 -0400845 self.thread_pool.AddTests(tests, parallel)
Edward Lemur21000eb2019-05-24 23:25:58 +0000846 # When self.parallel is True (i.e. --parallel is passed as an option)
847 # RunTests doesn't actually run tests. It adds them to a ThreadPool that
848 # will run all tests once all PRESUBMIT files are processed.
849 # Otherwise, it will run them and return the results.
850 if not self.parallel:
Edward Lesmes8e282792018-04-03 18:50:29 -0400851 msgs.extend(self.thread_pool.RunAsync())
852 return msgs
scottmg86099d72016-09-01 09:16:51 -0700853
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000854
nick@chromium.orgff526192013-06-10 19:30:26 +0000855class _DiffCache(object):
856 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000857 def __init__(self, upstream=None):
858 """Stores the upstream revision against which all diffs will be computed."""
859 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000860
861 def GetDiff(self, path, local_root):
862 """Get the diff for a particular path."""
863 raise NotImplementedError()
864
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700865 def GetOldContents(self, path, local_root):
866 """Get the old version for a particular path."""
867 raise NotImplementedError()
868
nick@chromium.orgff526192013-06-10 19:30:26 +0000869
nick@chromium.orgff526192013-06-10 19:30:26 +0000870class _GitDiffCache(_DiffCache):
871 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000872 def __init__(self, upstream):
873 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000874 self._diffs_by_file = None
875
876 def GetDiff(self, path, local_root):
877 if not self._diffs_by_file:
878 # Compute a single diff for all files and parse the output; should
879 # with git this is much faster than computing one diff for each file.
880 diffs = {}
881
882 # Don't specify any filenames below, because there are command line length
883 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000884 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
885 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000886
887 # This regex matches the path twice, separated by a space. Note that
888 # filename itself may contain spaces.
889 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
890 current_diff = []
891 keep_line_endings = True
892 for x in unified_diff.splitlines(keep_line_endings):
893 match = file_marker.match(x)
894 if match:
895 # Marks the start of a new per-file section.
896 diffs[match.group('filename')] = current_diff = [x]
897 elif x.startswith('diff --git'):
898 raise PresubmitFailure('Unexpected diff line: %s' % x)
899 else:
900 current_diff.append(x)
901
902 self._diffs_by_file = dict(
903 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
904
905 if path not in self._diffs_by_file:
906 raise PresubmitFailure(
907 'Unified diff did not contain entry for file %s' % path)
908
909 return self._diffs_by_file[path]
910
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700911 def GetOldContents(self, path, local_root):
912 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
913
nick@chromium.orgff526192013-06-10 19:30:26 +0000914
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000915class AffectedFile(object):
916 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000917
918 DIFF_CACHE = _DiffCache
919
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000920 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800921 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000922 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000923 self._path = path
924 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000925 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000926 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000927 self._cached_changed_contents = None
928 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000929 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700930 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000931
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000932 def LocalPath(self):
933 """Returns the path of this file on the local disk relative to client root.
Andrew Grieve92b8b992017-11-02 09:42:24 -0400934
935 This should be used for error messages but not for accessing files,
936 because presubmit checks are run with CWD=PresubmitLocalPath() (which is
937 often != client root).
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000938 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000939 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000940
941 def AbsoluteLocalPath(self):
942 """Returns the absolute path of this file on the local disk.
943 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000944 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000945
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000946 def Action(self):
947 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000948 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000949
agable0b65e732016-11-22 09:25:46 -0800950 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000951 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000952
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000953 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000954 raise NotImplementedError() # Implement when needed
955
agable0b65e732016-11-22 09:25:46 -0800956 def IsTextFile(self):
957 """An alias to IsTestableFile for backwards compatibility."""
958 return self.IsTestableFile()
959
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700960 def OldContents(self):
961 """Returns an iterator over the lines in the old version of file.
962
Daniel Cheng2da34fe2017-03-21 20:42:12 -0700963 The old version is the file before any modifications in the user's
Edward Lemura5799e32020-01-17 19:26:51 +0000964 workspace, i.e. the 'left hand side'.
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700965
966 Contents will be empty if the file is a directory or does not exist.
967 Note: The carriage returns (LF or CR) are stripped off.
968 """
969 return self._diff_cache.GetOldContents(self.LocalPath(),
970 self._local_root).splitlines()
971
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000972 def NewContents(self):
973 """Returns an iterator over the lines in the new version of file.
974
Edward Lemura5799e32020-01-17 19:26:51 +0000975 The new version is the file in the user's workspace, i.e. the 'right hand
976 side'.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000977
978 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000979 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000980 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000981 if self._cached_new_contents is None:
982 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800983 try:
984 self._cached_new_contents = gclient_utils.FileRead(
985 self.AbsoluteLocalPath(), 'rU').splitlines()
986 except IOError:
987 pass # File not found? That's fine; maybe it was deleted.
Greg Thompson30cde452021-06-01 16:38:47 +0000988 except UnicodeDecodeError as e:
Dirk Pranke6fc394f2021-05-26 23:25:14 +0000989 # log the filename since we're probably trying to read a binary
990 # file, and shouldn't be.
991 print('Error reading %s: %s' % (self.AbsoluteLocalPath(), e))
992 raise
993
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000994 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000995
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +0000996 def ChangedContents(self, keeplinebreaks=False):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000997 """Returns a list of tuples (line number, line text) of all new lines.
998
999 This relies on the scm diff output describing each changed code section
1000 with a line of the form
1001
1002 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
1003 """
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001004 # Don't return cached results when line breaks are requested.
1005 if not keeplinebreaks and self._cached_changed_contents is not None:
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +00001006 return self._cached_changed_contents[:]
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001007 result = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001008 line_num = 0
1009
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001010 # The keeplinebreaks parameter to splitlines must be True or else the
1011 # CheckForWindowsLineEndings presubmit will be a NOP.
1012 for line in self.GenerateScmDiff().splitlines(keeplinebreaks):
Edward Lemurac5c55f2020-02-29 00:17:16 +00001013 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
1014 if m:
1015 line_num = int(m.groups(1)[0])
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001016 continue
1017 if line.startswith('+') and not line.startswith('++'):
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001018 result.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001019 if not line.startswith('-'):
1020 line_num += 1
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001021 # Don't cache results with line breaks.
1022 if keeplinebreaks:
1023 return result;
1024 self._cached_changed_contents = result
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +00001025 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001026
maruel@chromium.org5de13972009-06-10 18:16:06 +00001027 def __str__(self):
1028 return self.LocalPath()
1029
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001030 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +00001031 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001032
maruel@chromium.org58407af2011-04-12 23:15:57 +00001033
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001034class GitAffectedFile(AffectedFile):
1035 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +00001036 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001037 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001038
nick@chromium.orgff526192013-06-10 19:30:26 +00001039 DIFF_CACHE = _GitDiffCache
1040
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001041 def __init__(self, *args, **kwargs):
1042 AffectedFile.__init__(self, *args, **kwargs)
1043 self._server_path = None
agable0b65e732016-11-22 09:25:46 -08001044 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001045
agable0b65e732016-11-22 09:25:46 -08001046 def IsTestableFile(self):
1047 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001048 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -08001049 # A deleted file is not testable.
1050 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001051 else:
agable0b65e732016-11-22 09:25:46 -08001052 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
1053 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001054
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001055
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001056class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001057 """Describe a change.
1058
1059 Used directly by the presubmit scripts to query the current change being
1060 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001061
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001062 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +00001063 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001064 self.KEY: equivalent to tags['KEY']
1065 """
1066
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001067 _AFFECTED_FILES = AffectedFile
1068
Edward Lemura5799e32020-01-17 19:26:51 +00001069 # Matches key/value (or 'tag') lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +00001070 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001071 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001072 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001073
maruel@chromium.org58407af2011-04-12 23:15:57 +00001074 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001075 self, name, description, local_root, files, issue, patchset, author,
1076 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001077 if files is None:
1078 files = []
1079 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +00001080 # Convert root into an absolute path.
1081 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001082 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001083 self.issue = issue
1084 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +00001085 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001086
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001087 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001088 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001089 self._description_without_tags = ''
1090 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001091
maruel@chromium.orge085d812011-10-10 19:49:15 +00001092 assert all(
1093 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
1094
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001095 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001096 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +00001097 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
1098 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +00001099 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001100
Edward Lesmes9ce03f82021-01-12 20:13:31 +00001101 def UpstreamBranch(self):
1102 """Returns the upstream branch for the change."""
1103 return self._upstream
1104
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001105 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001106 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001107 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001108
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001109 def DescriptionText(self):
1110 """Returns the user-entered changelist description, minus tags.
1111
Edward Lemura5799e32020-01-17 19:26:51 +00001112 Any line in the user-provided description starting with e.g. 'FOO='
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001113 (whitespace permitted before and around) is considered a tag line. Such
1114 lines are stripped out of the description this function returns.
1115 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001116 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001117
1118 def FullDescriptionText(self):
1119 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001120 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001121
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001122 def SetDescriptionText(self, description):
1123 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001124
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001125 Also updates the list of tags."""
1126 self._full_description = description
1127
1128 # From the description text, build up a dictionary of key/value pairs
Edward Lemura5799e32020-01-17 19:26:51 +00001129 # plus the description minus all key/value or 'tag' lines.
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001130 description_without_tags = []
1131 self.tags = {}
1132 for line in self._full_description.splitlines():
1133 m = self.TAG_LINE_RE.match(line)
1134 if m:
1135 self.tags[m.group('key')] = m.group('value')
1136 else:
1137 description_without_tags.append(line)
1138
1139 # Change back to text and remove whitespace at end.
1140 self._description_without_tags = (
1141 '\n'.join(description_without_tags).rstrip())
1142
Edward Lemur69bb8be2020-02-03 20:37:38 +00001143 def AddDescriptionFooter(self, key, value):
1144 """Adds the given footer to the change description.
1145
1146 Args:
1147 key: A string with the key for the git footer. It must conform to
1148 the git footers format (i.e. 'List-Of-Tokens') and will be case
1149 normalized so that each token is title-cased.
1150 value: A string with the value for the git footer.
1151 """
1152 description = git_footers.add_footer(
1153 self.FullDescriptionText(), git_footers.normalize_name(key), value)
1154 self.SetDescriptionText(description)
1155
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001156 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001157 """Returns the repository (checkout) root directory for this change,
1158 as an absolute path.
1159 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001160 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001161
1162 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001163 """Return tags directly as attributes on the object."""
Edward Lemura5799e32020-01-17 19:26:51 +00001164 if not re.match(r'^[A-Z_]*$', attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001165 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +00001166 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001167
Edward Lemur69bb8be2020-02-03 20:37:38 +00001168 def GitFootersFromDescription(self):
1169 """Return the git footers present in the description.
1170
1171 Returns:
1172 footers: A dict of {footer: [values]} containing a multimap of the footers
1173 in the change description.
1174 """
1175 return git_footers.parse_footers(self.FullDescriptionText())
1176
Aaron Gablefc03e672017-05-15 14:09:42 -07001177 def BugsFromDescription(self):
1178 """Returns all bugs referenced in the commit description."""
Sean McAllister1e509c52021-10-25 17:54:25 +00001179 bug_tags = ['BUG', 'FIXED']
1180
1181 tags = []
1182 for tag in bug_tags:
1183 values = self.tags.get(tag)
1184 if values:
1185 tags += [value.strip() for value in values.split(',')]
1186
Caleb Rouleauc0546b92019-02-22 06:12:57 +00001187 footers = []
Edward Lemur69bb8be2020-02-03 20:37:38 +00001188 parsed = self.GitFootersFromDescription()
Dan Beam62954042019-10-03 21:20:33 +00001189 unsplit_footers = parsed.get('Bug', []) + parsed.get('Fixed', [])
Caleb Rouleauc0546b92019-02-22 06:12:57 +00001190 for unsplit_footer in unsplit_footers:
1191 footers += [b.strip() for b in unsplit_footer.split(',')]
Aaron Gable12ef5012017-05-15 14:29:00 -07001192 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -07001193
1194 def ReviewersFromDescription(self):
1195 """Returns all reviewers listed in the commit description."""
Edward Lemura5799e32020-01-17 19:26:51 +00001196 # We don't support a 'R:' git-footer for reviewers; that is in metadata.
Aaron Gable12ef5012017-05-15 14:29:00 -07001197 tags = [r.strip() for r in self.tags.get('R', '').split(',') if r.strip()]
1198 return sorted(set(tags))
Aaron Gablefc03e672017-05-15 14:09:42 -07001199
1200 def TBRsFromDescription(self):
1201 """Returns all TBR reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -07001202 tags = [r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()]
Aaron Gable6e7ddb62020-05-27 22:23:29 +00001203 # TODO(crbug.com/839208): Remove support for 'Tbr:' when TBRs are
1204 # programmatically determined by self-CR+1s.
Edward Lemur69bb8be2020-02-03 20:37:38 +00001205 footers = self.GitFootersFromDescription().get('Tbr', [])
Aaron Gable12ef5012017-05-15 14:29:00 -07001206 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -07001207
Aaron Gable6e7ddb62020-05-27 22:23:29 +00001208 # TODO(crbug.com/753425): Delete these once we're sure they're unused.
Aaron Gablefc03e672017-05-15 14:09:42 -07001209 @property
1210 def BUG(self):
1211 return ','.join(self.BugsFromDescription())
1212 @property
1213 def R(self):
1214 return ','.join(self.ReviewersFromDescription())
1215 @property
1216 def TBR(self):
1217 return ','.join(self.TBRsFromDescription())
1218
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001219 def AllFiles(self, root=None):
1220 """List all files under source control in the repo."""
1221 raise NotImplementedError()
1222
agable0b65e732016-11-22 09:25:46 -08001223 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001224 """Returns a list of AffectedFile instances for all files in the change.
1225
1226 Args:
1227 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +00001228 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001229
1230 Returns:
1231 [AffectedFile(path, action), AffectedFile(path, action)]
1232 """
Edward Lemurb9830242019-10-30 22:19:20 +00001233 affected = list(filter(file_filter, self._affected_files))
sail@chromium.org5538e022011-05-12 17:53:16 +00001234
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001235 if include_deletes:
1236 return affected
Edward Lemurb9830242019-10-30 22:19:20 +00001237 return list(filter(lambda x: x.Action() != 'D', affected))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001238
John Budorick16162372018-04-18 10:39:53 -07001239 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +00001240 """Return a list of the existing text files in a change."""
1241 if include_deletes is not None:
Edward Lemura5799e32020-01-17 19:26:51 +00001242 warn('AffectedTeestableFiles(include_deletes=%s)'
1243 ' is deprecated and ignored' % str(include_deletes),
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001244 category=DeprecationWarning,
1245 stacklevel=2)
Edward Lemurb9830242019-10-30 22:19:20 +00001246 return list(filter(
1247 lambda x: x.IsTestableFile(),
1248 self.AffectedFiles(include_deletes=False, **kwargs)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001249
agable0b65e732016-11-22 09:25:46 -08001250 def AffectedTextFiles(self, include_deletes=None):
1251 """An alias to AffectedTestableFiles for backwards compatibility."""
1252 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001253
agable0b65e732016-11-22 09:25:46 -08001254 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001255 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001256 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001257
agable0b65e732016-11-22 09:25:46 -08001258 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001259 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001260 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001261
1262 def RightHandSideLines(self):
Edward Lemura5799e32020-01-17 19:26:51 +00001263 """An iterator over all text lines in 'new' version of changed files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001264
1265 Lists lines from new or modified text files in the change.
1266
1267 This is useful for doing line-by-line regex checks, like checking for
1268 trailing whitespace.
1269
1270 Yields:
1271 a 3 tuple:
1272 the AffectedFile instance of the current file;
1273 integer line number (1-based); and
1274 the contents of the line as a string.
1275 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001276 return _RightHandSideLinesImpl(
1277 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -08001278 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001279
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02001280 def OriginalOwnersFiles(self):
1281 """A map from path names of affected OWNERS files to their old content."""
1282 def owners_file_filter(f):
1283 return 'OWNERS' in os.path.split(f.LocalPath())[1]
1284 files = self.AffectedFiles(file_filter=owners_file_filter)
1285 return dict([(f.LocalPath(), f.OldContents()) for f in files])
1286
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001287
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001288class GitChange(Change):
1289 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001290 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001291
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001292 def AllFiles(self, root=None):
1293 """List all files under source control in the repo."""
1294 root = root or self.RepositoryRoot()
1295 return subprocess.check_output(
Aaron Gable7817f022017-12-12 09:43:17 -08001296 ['git', '-c', 'core.quotePath=false', 'ls-files', '--', '.'],
Josip Sokcevic0b123462021-06-08 20:41:32 +00001297 cwd=root).decode('utf-8', 'ignore').splitlines()
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001298
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001299
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001300def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001301 """Finds all presubmit files that apply to a given set of source files.
1302
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001303 If inherit-review-settings-ok is present right under root, looks for
1304 PRESUBMIT.py in directories enclosing root.
1305
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001306 Args:
1307 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001308 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001309
1310 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001311 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001312 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001313 files = [normpath(os.path.join(root, f)) for f in files]
1314
1315 # List all the individual directories containing files.
1316 directories = set([os.path.dirname(f) for f in files])
1317
1318 # Ignore root if inherit-review-settings-ok is present.
1319 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1320 root = None
1321
1322 # Collect all unique directories that may contain PRESUBMIT.py.
1323 candidates = set()
1324 for directory in directories:
1325 while True:
1326 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001327 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001328 candidates.add(directory)
1329 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001330 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001331 parent_dir = os.path.dirname(directory)
1332 if parent_dir == directory:
1333 # We hit the system root directory.
1334 break
1335 directory = parent_dir
1336
1337 # Look for PRESUBMIT.py in all candidate directories.
1338 results = []
1339 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -07001340 try:
1341 for f in os.listdir(directory):
1342 p = os.path.join(directory, f)
1343 if os.path.isfile(p) and re.match(
1344 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1345 results.append(p)
1346 except OSError:
1347 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001348
tobiasjs2836bcf2016-08-16 04:08:16 -07001349 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001350 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001351
1352
rmistry@google.com5626a922015-02-26 14:03:30 +00001353class GetPostUploadExecuter(object):
1354 @staticmethod
Edward Lemur016a0872020-02-04 22:13:28 +00001355 def ExecPresubmitScript(script_text, presubmit_path, gerrit_obj, change):
rmistry@google.com5626a922015-02-26 14:03:30 +00001356 """Executes PostUploadHook() from a single presubmit script.
1357
1358 Args:
1359 script_text: The text of the presubmit script.
1360 presubmit_path: Project script to run.
Edward Lemur016a0872020-02-04 22:13:28 +00001361 gerrit_obj: The GerritAccessor object.
rmistry@google.com5626a922015-02-26 14:03:30 +00001362 change: The Change object.
1363
1364 Return:
1365 A list of results objects.
1366 """
1367 context = {}
1368 try:
Raul Tambre09e64b42019-05-14 01:57:22 +00001369 exec(compile(script_text, 'PRESUBMIT.py', 'exec', dont_inherit=True),
1370 context)
Raul Tambre7c938462019-05-24 16:35:35 +00001371 except Exception as e:
rmistry@google.com5626a922015-02-26 14:03:30 +00001372 raise PresubmitFailure('"%s" had an exception.\n%s'
1373 % (presubmit_path, e))
1374
1375 function_name = 'PostUploadHook'
1376 if function_name not in context:
1377 return {}
1378 post_upload_hook = context[function_name]
1379 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1380 raise PresubmitFailure(
1381 'Expected function "PostUploadHook" to take three arguments.')
Edward Lemur016a0872020-02-04 22:13:28 +00001382 return post_upload_hook(gerrit_obj, change, OutputApi(False))
rmistry@google.com5626a922015-02-26 14:03:30 +00001383
1384
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001385def _MergeMasters(masters1, masters2):
1386 """Merges two master maps. Merges also the tests of each builder."""
1387 result = {}
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001388 for (master, builders) in itertools.chain(masters1.items(),
1389 masters2.items()):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001390 new_builders = result.setdefault(master, {})
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001391 for (builder, tests) in builders.items():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001392 new_builders.setdefault(builder, set([])).update(tests)
1393 return result
1394
1395
rmistry@google.com5626a922015-02-26 14:03:30 +00001396def DoPostUploadExecuter(change,
Edward Lemur016a0872020-02-04 22:13:28 +00001397 gerrit_obj,
Edward Lemur6eb1d322020-02-27 22:20:15 +00001398 verbose):
rmistry@google.com5626a922015-02-26 14:03:30 +00001399 """Execute the post upload hook.
1400
1401 Args:
1402 change: The Change object.
Edward Lemur016a0872020-02-04 22:13:28 +00001403 gerrit_obj: The GerritAccessor object.
rmistry@google.com5626a922015-02-26 14:03:30 +00001404 verbose: Prints debug info.
rmistry@google.com5626a922015-02-26 14:03:30 +00001405 """
1406 presubmit_files = ListRelevantPresubmitFiles(
Edward Lemur6eb1d322020-02-27 22:20:15 +00001407 change.LocalPaths(), change.RepositoryRoot())
rmistry@google.com5626a922015-02-26 14:03:30 +00001408 if not presubmit_files and verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001409 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
rmistry@google.com5626a922015-02-26 14:03:30 +00001410 results = []
1411 executer = GetPostUploadExecuter()
1412 # The root presubmit file should be executed after the ones in subdirectories.
1413 # i.e. the specific post upload hooks should run before the general ones.
1414 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1415 presubmit_files.reverse()
1416
1417 for filename in presubmit_files:
1418 filename = os.path.abspath(filename)
1419 if verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001420 sys.stdout.write('Running %s\n' % filename)
rmistry@google.com5626a922015-02-26 14:03:30 +00001421 # Accept CRLF presubmit script.
1422 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1423 results.extend(executer.ExecPresubmitScript(
Edward Lemur016a0872020-02-04 22:13:28 +00001424 presubmit_script, filename, gerrit_obj, change))
rmistry@google.com5626a922015-02-26 14:03:30 +00001425
Edward Lemur6eb1d322020-02-27 22:20:15 +00001426 if not results:
1427 return 0
1428
1429 sys.stdout.write('\n')
1430 sys.stdout.write('** Post Upload Hook Messages **\n')
1431
1432 exit_code = 0
1433 for result in results:
1434 if result.fatal:
1435 exit_code = 1
1436 result.handle()
1437 sys.stdout.write('\n')
1438
1439 return exit_code
rmistry@google.com5626a922015-02-26 14:03:30 +00001440
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001441class PresubmitExecuter(object):
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001442 def __init__(self, change, committing, verbose, gerrit_obj, dry_run=None,
Dirk Pranke6f0df682021-06-25 00:42:33 +00001443 thread_pool=None, parallel=False, use_python3=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001444 """
1445 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001446 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001447 committing: True if 'git cl land' is running, False if 'git cl upload' is.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001448 gerrit_obj: provides basic Gerrit codereview functionality.
1449 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001450 parallel: if true, all tests reported via input_api.RunTests for all
1451 PRESUBMIT files will be run in parallel.
Dirk Pranke6f0df682021-06-25 00:42:33 +00001452 use_python3: if true, will use python3 instead of python2 by default
1453 if USE_PYTHON3 is not specified.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001454 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001455 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001456 self.committing = committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001457 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001458 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001459 self.dry_run = dry_run
Daniel Cheng7227d212017-11-17 08:12:37 -08001460 self.more_cc = []
Edward Lesmes8e282792018-04-03 18:50:29 -04001461 self.thread_pool = thread_pool
1462 self.parallel = parallel
Dirk Pranke6f0df682021-06-25 00:42:33 +00001463 self.use_python3 = use_python3
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001464
1465 def ExecPresubmitScript(self, script_text, presubmit_path):
1466 """Executes a single presubmit script.
1467
1468 Args:
1469 script_text: The text of the presubmit script.
1470 presubmit_path: The path to the presubmit file (this will be reported via
1471 input_api.PresubmitLocalPath()).
1472
1473 Return:
1474 A list of result objects, empty if no problems.
1475 """
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001476
chase@chromium.org8e416c82009-10-06 04:30:44 +00001477 # Change to the presubmit file's directory to support local imports.
1478 main_path = os.getcwd()
Saagar Sanghavi98b332f2020-07-31 17:19:15 +00001479 presubmit_dir = os.path.dirname(presubmit_path)
1480 os.chdir(presubmit_dir)
chase@chromium.org8e416c82009-10-06 04:30:44 +00001481
1482 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001483 input_api = InputApi(self.change, presubmit_path, self.committing,
Aaron Gable668c1d82018-04-03 10:19:16 -07001484 self.verbose, gerrit_obj=self.gerrit,
Edward Lesmes8e282792018-04-03 18:50:29 -04001485 dry_run=self.dry_run, thread_pool=self.thread_pool,
1486 parallel=self.parallel)
Daniel Cheng7227d212017-11-17 08:12:37 -08001487 output_api = OutputApi(self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001488 context = {}
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001489
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001490 # Try to figure out whether these presubmit checks should be run under
1491 # python2 or python3. We need to do this without actually trying to
1492 # compile the text, since the text might compile in one but not the
1493 # other.
Dirk Pranke6f0df682021-06-25 00:42:33 +00001494 m = re.search('^USE_PYTHON3 = (True|False)$', script_text,
1495 flags=re.MULTILINE)
1496 if m:
Josip Sokcevic6635baf2021-11-19 02:04:39 +00001497 use_python3 = m.group(1) == 'True'
Dirk Pranke6f0df682021-06-25 00:42:33 +00001498 else:
Josip Sokcevic6635baf2021-11-19 02:04:39 +00001499 use_python3 = self.use_python3
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001500 if (((sys.version_info.major == 2) and use_python3) or
1501 ((sys.version_info.major == 3) and not use_python3)):
1502 return []
1503
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001504 try:
Raul Tambre09e64b42019-05-14 01:57:22 +00001505 exec(compile(script_text, 'PRESUBMIT.py', 'exec', dont_inherit=True),
1506 context)
Raul Tambre7c938462019-05-24 16:35:35 +00001507 except Exception as e:
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001508 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001509
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001510 context['__args'] = (input_api, output_api)
Ben Pastene8351dc12020-08-06 05:01:35 +00001511
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001512 # Get path of presubmit directory relative to repository root.
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001513 # Always use forward slashes, so that path is same in *nix and Windows
1514 root = input_api.change.RepositoryRoot()
1515 rel_path = os.path.relpath(presubmit_dir, root)
1516 rel_path = rel_path.replace(os.path.sep, '/')
Ben Pastene8351dc12020-08-06 05:01:35 +00001517
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001518 # Get the URL of git remote origin and use it to identify host and project
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001519 host = project = ''
1520 if self.gerrit:
1521 host = self.gerrit.host or ''
1522 project = self.gerrit.project or ''
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001523
1524 # Prefix for test names
1525 prefix = 'presubmit:%s/%s:%s/' % (host, project, rel_path)
1526
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001527 # Perform all the desired presubmit checks.
1528 results = []
Ben Pastene8351dc12020-08-06 05:01:35 +00001529
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001530 try:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001531 version = [
1532 int(x) for x in context.get('PRESUBMIT_VERSION', '0.0.0').split('.')
1533 ]
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001534
Scott Leecc2fe9b2020-11-19 19:38:06 +00001535 with rdb_wrapper.client(prefix) as sink:
1536 if version >= [2, 0, 0]:
1537 for function_name in context:
1538 if not function_name.startswith('Check'):
1539 continue
1540 if function_name.endswith('Commit') and not self.committing:
1541 continue
1542 if function_name.endswith('Upload') and self.committing:
1543 continue
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001544 logging.debug('Running %s in %s', function_name, presubmit_path)
1545 results.extend(
Scott Leecc2fe9b2020-11-19 19:38:06 +00001546 self._run_check_function(function_name, context, sink))
1547 logging.debug('Running %s done.', function_name)
1548 self.more_cc.extend(output_api.more_cc)
1549
1550 else: # Old format
1551 if self.committing:
1552 function_name = 'CheckChangeOnCommit'
1553 else:
1554 function_name = 'CheckChangeOnUpload'
1555 if function_name in context:
1556 logging.debug('Running %s in %s', function_name, presubmit_path)
1557 results.extend(
1558 self._run_check_function(function_name, context, sink))
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001559 logging.debug('Running %s done.', function_name)
1560 self.more_cc.extend(output_api.more_cc)
1561
1562 finally:
1563 for f in input_api._named_temporary_files:
1564 os.remove(f)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001565
chase@chromium.org8e416c82009-10-06 04:30:44 +00001566 # Return the process to the original working directory.
1567 os.chdir(main_path)
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001568 return results
1569
Scott Leecc2fe9b2020-11-19 19:38:06 +00001570 def _run_check_function(self, function_name, context, sink=None):
1571 """Evaluates and returns the result of a given presubmit function.
1572
1573 If sink is given, the result of the presubmit function will be reported
1574 to the ResultSink.
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001575
1576 Args:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001577 function_name: the name of the presubmit function to evaluate
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001578 context: a context dictionary in which the function will be evaluated
Scott Leecc2fe9b2020-11-19 19:38:06 +00001579 sink: an instance of ResultSink. None, by default.
1580 Returns:
1581 the result of the presubmit function call.
1582 """
1583 start_time = time_time()
1584 try:
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001585 result = eval(function_name + '(*__args)', context)
1586 self._check_result_type(result)
Allen Webbfe7d7092021-05-18 02:05:49 +00001587 except Exception:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001588 if sink:
1589 elapsed_time = time_time() - start_time
1590 sink.report(function_name, rdb_wrapper.STATUS_FAIL, elapsed_time)
Allen Webbfe7d7092021-05-18 02:05:49 +00001591 # TODO(crbug.com/953884): replace reraise with native py3:
Allen Webbb65bbfe2021-05-11 21:22:01 +00001592 # raise .. from e
Allen Webbfe7d7092021-05-18 02:05:49 +00001593 e_type, e_value, e_tb = sys.exc_info()
Dirk Pranke6fc394f2021-05-26 23:25:14 +00001594 print('Evaluation of %s failed: %s' % (function_name, e_value))
1595 six.reraise(e_type, e_value, e_tb)
Scott Leecc2fe9b2020-11-19 19:38:06 +00001596
Bruce Dawson2bdc49f2021-05-21 19:13:03 +00001597 elapsed_time = time_time() - start_time
1598 if elapsed_time > 10.0:
1599 sys.stdout.write(
1600 '%s took %.1fs to run.\n' % (function_name, elapsed_time))
Scott Leecc2fe9b2020-11-19 19:38:06 +00001601 if sink:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001602 status = rdb_wrapper.STATUS_PASS
1603 if any(r.fatal for r in result):
1604 status = rdb_wrapper.STATUS_FAIL
1605 sink.report(function_name, status, elapsed_time)
1606
1607 return result
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001608
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001609 def _check_result_type(self, result):
1610 """Helper function which ensures result is a list, and all elements are
1611 instances of OutputApi.PresubmitResult"""
1612 if not isinstance(result, (tuple, list)):
1613 raise PresubmitFailure('Presubmit functions must return a tuple or list')
1614 if not all(isinstance(res, OutputApi.PresubmitResult) for res in result):
1615 raise PresubmitFailure(
1616 'All presubmit results must be of types derived from '
1617 'output_api.PresubmitResult')
1618
1619
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001620def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001621 committing,
1622 verbose,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001623 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001624 may_prompt,
Aaron Gable668c1d82018-04-03 10:19:16 -07001625 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04001626 dry_run=None,
Debrian Figueroadd2737e2019-06-21 23:50:13 +00001627 parallel=False,
Dirk Pranke6f0df682021-06-25 00:42:33 +00001628 json_output=None,
1629 use_python3=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001630 """Runs all presubmit checks that apply to the files in the change.
1631
1632 This finds all PRESUBMIT.py files in directories enclosing the files in the
1633 change (up to the repository root) and calls the relevant entrypoint function
1634 depending on whether the change is being committed or uploaded.
1635
1636 Prints errors, warnings and notifications. Prompts the user for warnings
1637 when needed.
1638
1639 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001640 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001641 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001642 verbose: Prints debug info.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001643 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001644 may_prompt: Enable (y/n) questions on warning or error. If False,
1645 any questions are answered with yes by default.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001646 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001647 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001648 parallel: if true, all tests specified by input_api.RunTests in all
1649 PRESUBMIT files will be run in parallel.
Dirk Pranke6f0df682021-06-25 00:42:33 +00001650 use_python3: if true, default to using Python3 for presubmit checks
1651 rather than Python2.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001652 Return:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001653 1 if presubmit checks failed or 0 otherwise.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001654 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001655 old_environ = os.environ
1656 try:
1657 # Make sure python subprocesses won't generate .pyc files.
1658 os.environ = os.environ.copy()
Edward Lemurb9830242019-10-30 22:19:20 +00001659 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001660
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001661 python_version = 'Python %s' % sys.version_info.major
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001662 if committing:
Dirk Pranke6fc394f2021-05-26 23:25:14 +00001663 sys.stdout.write('Running %s presubmit commit checks ...\n' %
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001664 python_version)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001665 else:
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001666 sys.stdout.write('Running %s presubmit upload checks ...\n' %
1667 python_version)
Edward Lemurecc27072020-01-06 16:42:34 +00001668 start_time = time_time()
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001669 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001670 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001671 if not presubmit_files and verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001672 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001673 results = []
Edward Lesmes8e282792018-04-03 18:50:29 -04001674 thread_pool = ThreadPool()
Edward Lemur7e3c67f2018-07-20 20:52:49 +00001675 executer = PresubmitExecuter(change, committing, verbose, gerrit_obj,
Dirk Pranke6f0df682021-06-25 00:42:33 +00001676 dry_run, thread_pool, parallel, use_python3)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001677 if default_presubmit:
1678 if verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001679 sys.stdout.write('Running default presubmit script.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001680 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1681 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1682 for filename in presubmit_files:
1683 filename = os.path.abspath(filename)
1684 if verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001685 sys.stdout.write('Running %s\n' % filename)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001686 # Accept CRLF presubmit script.
1687 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1688 results += executer.ExecPresubmitScript(presubmit_script, filename)
Edward Lesmes8e282792018-04-03 18:50:29 -04001689 results += thread_pool.RunAsync()
1690
Edward Lemur6eb1d322020-02-27 22:20:15 +00001691 messages = {}
1692 should_prompt = False
1693 presubmits_failed = False
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001694 for result in results:
1695 if result.fatal:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001696 presubmits_failed = True
1697 messages.setdefault('ERRORS', []).append(result)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001698 elif result.should_prompt:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001699 should_prompt = True
1700 messages.setdefault('Warnings', []).append(result)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001701 else:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001702 messages.setdefault('Messages', []).append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001703
Edward Lemur6eb1d322020-02-27 22:20:15 +00001704 sys.stdout.write('\n')
1705 for name, items in messages.items():
1706 sys.stdout.write('** Presubmit %s **\n' % name)
1707 for item in items:
1708 item.handle()
1709 sys.stdout.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001710
Edward Lemurecc27072020-01-06 16:42:34 +00001711 total_time = time_time() - start_time
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001712 if total_time > 1.0:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001713 sys.stdout.write(
1714 'Presubmit checks took %.1fs to calculate.\n\n' % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001715
Edward Lemur6eb1d322020-02-27 22:20:15 +00001716 if not should_prompt and not presubmits_failed:
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001717 sys.stdout.write('%s presubmit checks passed.\n' % python_version)
Edward Lemur6eb1d322020-02-27 22:20:15 +00001718 elif should_prompt:
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001719 sys.stdout.write('There were %s presubmit warnings. ' % python_version)
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001720 if may_prompt:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001721 presubmits_failed = not prompt_should_continue(
1722 'Are you sure you wish to continue? (y/N): ')
Garrett Beatyacb807e2020-08-28 18:17:24 +00001723 else:
1724 sys.stdout.write('\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001725
Edward Lemur1dc66e12020-02-21 21:36:34 +00001726 if json_output:
1727 # Write the presubmit results to json output
1728 presubmit_results = {
1729 'errors': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001730 error.json_format()
1731 for error in messages.get('ERRORS', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001732 ],
1733 'notifications': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001734 notification.json_format()
1735 for notification in messages.get('Messages', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001736 ],
1737 'warnings': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001738 warning.json_format()
1739 for warning in messages.get('Warnings', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001740 ],
Edward Lemur6eb1d322020-02-27 22:20:15 +00001741 'more_cc': executer.more_cc,
Edward Lemur1dc66e12020-02-21 21:36:34 +00001742 }
1743
1744 gclient_utils.FileWrite(
1745 json_output, json.dumps(presubmit_results, sort_keys=True))
1746
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001747 global _ASKED_FOR_FEEDBACK
1748 # Ask for feedback one time out of 5.
1749 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
Edward Lemur6eb1d322020-02-27 22:20:15 +00001750 sys.stdout.write(
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001751 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1752 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1753 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001754 _ASKED_FOR_FEEDBACK = True
Edward Lemur6eb1d322020-02-27 22:20:15 +00001755
1756 return 1 if presubmits_failed else 0
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001757 finally:
1758 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001759
1760
Edward Lemur50984a62020-02-06 18:10:18 +00001761def _scan_sub_dirs(mask, recursive):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001762 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001763 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001764
1765 results = []
1766 for root, dirs, files in os.walk('.'):
1767 if '.svn' in dirs:
1768 dirs.remove('.svn')
1769 if '.git' in dirs:
1770 dirs.remove('.git')
1771 for name in files:
1772 if fnmatch.fnmatch(name, mask):
1773 results.append(os.path.join(root, name))
1774 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001775
1776
Edward Lemur50984a62020-02-06 18:10:18 +00001777def _parse_files(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001778 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001779 files = []
1780 for arg in args:
Edward Lemur50984a62020-02-06 18:10:18 +00001781 files.extend([('M', f) for f in _scan_sub_dirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001782 return files
1783
1784
Edward Lemur50984a62020-02-06 18:10:18 +00001785def _parse_change(parser, options):
1786 """Process change options.
1787
1788 Args:
1789 parser: The parser used to parse the arguments from command line.
1790 options: The arguments parsed from command line.
1791 Returns:
1792 A GitChange if the change root is a git repository, or a Change otherwise.
1793 """
1794 if options.files and options.all_files:
1795 parser.error('<files> cannot be specified when --all-files is set.')
1796
Edward Lesmes44ea3ff2020-02-05 00:14:30 +00001797 change_scm = scm.determine_scm(options.root)
Edward Lemur50984a62020-02-06 18:10:18 +00001798 if change_scm != 'git' and not options.files:
1799 parser.error('<files> is not optional for unversioned directories.')
1800
1801 if options.files:
1802 change_files = _parse_files(options.files, options.recursive)
1803 elif options.all_files:
1804 change_files = [('M', f) for f in scm.GIT.GetAllFiles(options.root)]
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001805 else:
Edward Lemur50984a62020-02-06 18:10:18 +00001806 change_files = scm.GIT.CaptureStatus(
Edward Lemur7f6dec02020-02-06 20:23:58 +00001807 options.root, options.upstream or None)
Edward Lemur50984a62020-02-06 18:10:18 +00001808
1809 logging.info('Found %d file(s).', len(change_files))
1810
1811 change_class = GitChange if change_scm == 'git' else Change
1812 return change_class(
1813 options.name,
1814 options.description,
1815 options.root,
1816 change_files,
1817 options.issue,
1818 options.patchset,
1819 options.author,
1820 upstream=options.upstream)
1821
1822
1823def _parse_gerrit_options(parser, options):
1824 """Process gerrit options.
1825
1826 SIDE EFFECTS: Modifies options.author and options.description from Gerrit if
1827 options.gerrit_fetch is set.
1828
1829 Args:
1830 parser: The parser used to parse the arguments from command line.
1831 options: The arguments parsed from command line.
1832 Returns:
Stephen Martinisfb09de22021-02-25 03:41:13 +00001833 A GerritAccessor object if options.gerrit_url is set, or None otherwise.
Edward Lemur50984a62020-02-06 18:10:18 +00001834 """
1835 gerrit_obj = None
Stephen Martinisfb09de22021-02-25 03:41:13 +00001836 if options.gerrit_url:
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001837 gerrit_obj = GerritAccessor(
1838 url=options.gerrit_url,
1839 project=options.gerrit_project,
1840 branch=options.gerrit_branch)
Edward Lemur50984a62020-02-06 18:10:18 +00001841
1842 if not options.gerrit_fetch:
1843 return gerrit_obj
1844
1845 if not options.gerrit_url or not options.issue or not options.patchset:
1846 parser.error(
1847 '--gerrit_fetch requires --gerrit_url, --issue and --patchset.')
1848
1849 options.author = gerrit_obj.GetChangeOwner(options.issue)
1850 options.description = gerrit_obj.GetChangeDescription(
1851 options.issue, options.patchset)
1852
1853 logging.info('Got author: "%s"', options.author)
1854 logging.info('Got description: """\n%s\n"""', options.description)
1855
1856 return gerrit_obj
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001857
1858
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001859@contextlib.contextmanager
1860def canned_check_filter(method_names):
1861 filtered = {}
1862 try:
1863 for method_name in method_names:
1864 if not hasattr(presubmit_canned_checks, method_name):
Gavin Make6a62332020-12-04 21:57:10 +00001865 logging.warning('Skipping unknown "canned" check %s' % method_name)
Aaron Gableecee74c2018-04-02 15:13:08 -07001866 continue
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001867 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1868 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1869 yield
1870 finally:
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001871 for name, method in filtered.items():
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001872 setattr(presubmit_canned_checks, name, method)
1873
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001874
sbc@chromium.org013731e2015-02-26 18:28:43 +00001875def main(argv=None):
Edward Lemura5799e32020-01-17 19:26:51 +00001876 parser = argparse.ArgumentParser(usage='%(prog)s [options] <files...>')
1877 hooks = parser.add_mutually_exclusive_group()
1878 hooks.add_argument('-c', '--commit', action='store_true',
1879 help='Use commit instead of upload checks.')
1880 hooks.add_argument('-u', '--upload', action='store_false', dest='commit',
1881 help='Use upload instead of commit checks.')
Edward Lemur75526302020-02-27 22:31:05 +00001882 hooks.add_argument('--post_upload', action='store_true',
1883 help='Run post-upload commit hooks.')
Edward Lemura5799e32020-01-17 19:26:51 +00001884 parser.add_argument('-r', '--recursive', action='store_true',
1885 help='Act recursively.')
1886 parser.add_argument('-v', '--verbose', action='count', default=0,
1887 help='Use 2 times for more debug info.')
1888 parser.add_argument('--name', default='no name')
1889 parser.add_argument('--author')
Edward Lemur227d5102020-02-25 23:45:35 +00001890 desc = parser.add_mutually_exclusive_group()
1891 desc.add_argument('--description', default='', help='The change description.')
1892 desc.add_argument('--description_file',
1893 help='File to read change description from.')
Edward Lemura5799e32020-01-17 19:26:51 +00001894 parser.add_argument('--issue', type=int, default=0)
1895 parser.add_argument('--patchset', type=int, default=0)
1896 parser.add_argument('--root', default=os.getcwd(),
1897 help='Search for PRESUBMIT.py up to this directory. '
1898 'If inherit-review-settings-ok is present in this '
1899 'directory, parent directories up to the root file '
1900 'system directories will also be searched.')
1901 parser.add_argument('--upstream',
1902 help='Git only: the base ref or upstream branch against '
1903 'which the diff should be computed.')
1904 parser.add_argument('--default_presubmit')
1905 parser.add_argument('--may_prompt', action='store_true', default=False)
1906 parser.add_argument('--skip_canned', action='append', default=[],
1907 help='A list of checks to skip which appear in '
1908 'presubmit_canned_checks. Can be provided multiple times '
1909 'to skip multiple canned checks.')
1910 parser.add_argument('--dry_run', action='store_true', help=argparse.SUPPRESS)
1911 parser.add_argument('--gerrit_url', help=argparse.SUPPRESS)
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001912 parser.add_argument('--gerrit_project', help=argparse.SUPPRESS)
1913 parser.add_argument('--gerrit_branch', help=argparse.SUPPRESS)
Edward Lemura5799e32020-01-17 19:26:51 +00001914 parser.add_argument('--gerrit_fetch', action='store_true',
1915 help=argparse.SUPPRESS)
1916 parser.add_argument('--parallel', action='store_true',
1917 help='Run all tests specified by input_api.RunTests in '
1918 'all PRESUBMIT files in parallel.')
1919 parser.add_argument('--json_output',
1920 help='Write presubmit errors to json output.')
Edward Lemur1dc66e12020-02-21 21:36:34 +00001921 parser.add_argument('--all_files', action='store_true',
Edward Lemur50984a62020-02-06 18:10:18 +00001922 help='Mark all files under source control as modified.')
Edward Lemura5799e32020-01-17 19:26:51 +00001923 parser.add_argument('files', nargs='*',
1924 help='List of files to be marked as modified when '
1925 'executing presubmit or post-upload hooks. fnmatch '
1926 'wildcards can also be used.')
Dirk Pranke6f0df682021-06-25 00:42:33 +00001927 parser.add_argument('--use-python3', action='store_true',
1928 help='Use python3 for presubmit checks by default')
Edward Lemura5799e32020-01-17 19:26:51 +00001929 options = parser.parse_args(argv)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001930
Erik Staabcca5c492020-04-16 17:40:07 +00001931 log_level = logging.ERROR
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001932 if options.verbose >= 2:
Erik Staabcca5c492020-04-16 17:40:07 +00001933 log_level = logging.DEBUG
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001934 elif options.verbose:
Erik Staabcca5c492020-04-16 17:40:07 +00001935 log_level = logging.INFO
1936 log_format = ('[%(levelname).1s%(asctime)s %(process)d %(thread)d '
1937 '%(filename)s] %(message)s')
1938 logging.basicConfig(format=log_format, level=log_level)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001939
Edward Lemur227d5102020-02-25 23:45:35 +00001940 if options.description_file:
1941 options.description = gclient_utils.FileRead(options.description_file)
Edward Lemur50984a62020-02-06 18:10:18 +00001942 gerrit_obj = _parse_gerrit_options(parser, options)
1943 change = _parse_change(parser, options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001944
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001945 try:
Edward Lemur75526302020-02-27 22:31:05 +00001946 if options.post_upload:
1947 return DoPostUploadExecuter(
1948 change,
1949 gerrit_obj,
1950 options.verbose)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001951 with canned_check_filter(options.skip_canned):
Edward Lemur6eb1d322020-02-27 22:20:15 +00001952 return DoPresubmitChecks(
Edward Lemur50984a62020-02-06 18:10:18 +00001953 change,
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001954 options.commit,
1955 options.verbose,
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001956 options.default_presubmit,
1957 options.may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001958 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04001959 options.dry_run,
Debrian Figueroadd2737e2019-06-21 23:50:13 +00001960 options.parallel,
Dirk Pranke6f0df682021-06-25 00:42:33 +00001961 options.json_output,
1962 options.use_python3)
Raul Tambre7c938462019-05-24 16:35:35 +00001963 except PresubmitFailure as e:
Raul Tambre80ee78e2019-05-06 22:41:05 +00001964 print(e, file=sys.stderr)
1965 print('Maybe your depot_tools is out of date?', file=sys.stderr)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001966 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001967
1968
1969if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001970 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001971 try:
1972 sys.exit(main())
1973 except KeyboardInterrupt:
1974 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001975 sys.exit(2)