blob: a011d13c7937464b3614a5b2d00f9b7b80e3dc62 [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)
209 if timer.completed:
210 stdout = 'Process timed out after %ss\n%s' % (self.timeout, stdout)
Josip Sokcevicb4a7cff2021-06-09 22:56:42 +0000211 return p.returncode, stdout.decode('utf-8', 'ignore');
Edward Lemurecc27072020-01-06 16:42:34 +0000212
213 def CallCommand(self, test):
214 """Runs an external program.
215
Edward Lemura5799e32020-01-17 19:26:51 +0000216 This function converts invocation of .py files and invocations of 'python'
Edward Lemurecc27072020-01-06 16:42:34 +0000217 to vpython invocations.
218 """
219 cmd = self._GetCommand(test)
Edward Lesmes8e282792018-04-03 18:50:29 -0400220 try:
Edward Lemurecc27072020-01-06 16:42:34 +0000221 start = time_time()
222 returncode, stdout = self._RunWithTimeout(cmd, test.stdin, test.kwargs)
223 duration = time_time() - start
Edward Lemur7cf94382019-11-15 22:36:41 +0000224 except Exception:
Edward Lemurecc27072020-01-06 16:42:34 +0000225 duration = time_time() - start
Edward Lesmes8e282792018-04-03 18:50:29 -0400226 return test.message(
Edward Lemur7cf94382019-11-15 22:36:41 +0000227 '%s\n%s exec failure (%4.2fs)\n%s' % (
228 test.name, ' '.join(cmd), duration, traceback.format_exc()))
Edward Lemurde9e3ca2019-10-24 21:13:31 +0000229
Edward Lemurecc27072020-01-06 16:42:34 +0000230 if returncode != 0:
Edward Lesmes8e282792018-04-03 18:50:29 -0400231 return test.message(
Edward Lemur7cf94382019-11-15 22:36:41 +0000232 '%s\n%s (%4.2fs) failed\n%s' % (
233 test.name, ' '.join(cmd), duration, stdout))
Edward Lemurecc27072020-01-06 16:42:34 +0000234
Edward Lesmes8e282792018-04-03 18:50:29 -0400235 if test.info:
Edward Lemur7cf94382019-11-15 22:36:41 +0000236 return test.info('%s\n%s (%4.2fs)' % (test.name, ' '.join(cmd), duration))
Edward Lesmes8e282792018-04-03 18:50:29 -0400237
238 def AddTests(self, tests, parallel=True):
239 if parallel:
240 self._tests.extend(tests)
241 else:
242 self._nonparallel_tests.extend(tests)
243
244 def RunAsync(self):
245 self._messages = []
246
247 def _WorkerFn():
248 while True:
249 test = None
250 with self._tests_lock:
251 if not self._tests:
252 break
253 test = self._tests.pop()
254 result = self.CallCommand(test)
255 if result:
256 with self._messages_lock:
257 self._messages.append(result)
258
259 def _StartDaemon():
260 t = threading.Thread(target=_WorkerFn)
261 t.daemon = True
262 t.start()
263 return t
264
265 while self._nonparallel_tests:
266 test = self._nonparallel_tests.pop()
267 result = self.CallCommand(test)
268 if result:
269 self._messages.append(result)
270
271 if self._tests:
272 threads = [_StartDaemon() for _ in range(self._pool_size)]
273 for worker in threads:
274 worker.join()
275
276 return self._messages
277
278
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000279def normpath(path):
280 '''Version of os.path.normpath that also changes backward slashes to
281 forward slashes when not running on Windows.
282 '''
283 # This is safe to always do because the Windows version of os.path.normpath
284 # will replace forward slashes with backward slashes.
285 path = path.replace(os.sep, '/')
286 return os.path.normpath(path)
287
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000288
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000289def _RightHandSideLinesImpl(affected_files):
290 """Implements RightHandSideLines for InputApi and GclChange."""
291 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000292 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000293 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000294 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000295
296
Edward Lemur6eb1d322020-02-27 22:20:15 +0000297def prompt_should_continue(prompt_string):
298 sys.stdout.write(prompt_string)
Dirk Pranke7288f882021-06-03 18:01:30 +0000299 sys.stdout.flush()
Edward Lemur6eb1d322020-02-27 22:20:15 +0000300 response = sys.stdin.readline().strip().lower()
301 return response in ('y', 'yes')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000302
303
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000304# Top level object so multiprocessing can pickle
305# Public access through OutputApi object.
306class _PresubmitResult(object):
307 """Base class for result objects."""
308 fatal = False
309 should_prompt = False
310
311 def __init__(self, message, items=None, long_text=''):
312 """
313 message: A short one-line message to indicate errors.
314 items: A list of short strings to indicate where errors occurred.
315 long_text: multi-line text output, e.g. from another tool
316 """
317 self._message = message
318 self._items = items or []
Tom McKee61c72652021-07-20 11:56:32 +0000319 self._long_text = _PresubmitResult._ensure_str(long_text.rstrip())
320
321 @staticmethod
322 def _ensure_str(val):
323 """
324 val: A "stringish" value. Can be any of str, unicode or bytes.
325 returns: A str after applying encoding/decoding as needed.
326 Assumes/uses UTF-8 for relevant inputs/outputs.
327
328 We'd prefer to use six.ensure_str but our copy of six is old :(
329 """
330 if isinstance(val, str):
331 return val
332 if six.PY2 and isinstance(val, unicode):
333 return val.encode()
334 elif six.PY3 and isinstance(val, bytes):
335 return val.decode()
336 raise ValueError("Unknown string type %s" % type(val))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000337
Edward Lemur6eb1d322020-02-27 22:20:15 +0000338 def handle(self):
339 sys.stdout.write(self._message)
340 sys.stdout.write('\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000341 for index, item in enumerate(self._items):
Edward Lemur6eb1d322020-02-27 22:20:15 +0000342 sys.stdout.write(' ')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000343 # Write separately in case it's unicode.
Edward Lemur6eb1d322020-02-27 22:20:15 +0000344 sys.stdout.write(str(item))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000345 if index < len(self._items) - 1:
Edward Lemur6eb1d322020-02-27 22:20:15 +0000346 sys.stdout.write(' \\')
347 sys.stdout.write('\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000348 if self._long_text:
Edward Lemur6eb1d322020-02-27 22:20:15 +0000349 sys.stdout.write('\n***************\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000350 # Write separately in case it's unicode.
Tom McKee61c72652021-07-20 11:56:32 +0000351 sys.stdout.write(self._long_text)
Edward Lemur6eb1d322020-02-27 22:20:15 +0000352 sys.stdout.write('\n***************\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000353
Debrian Figueroadd2737e2019-06-21 23:50:13 +0000354 def json_format(self):
355 return {
356 'message': self._message,
Debrian Figueroa6095d402019-06-28 18:47:18 +0000357 'items': [str(item) for item in self._items],
Debrian Figueroadd2737e2019-06-21 23:50:13 +0000358 'long_text': self._long_text,
359 'fatal': self.fatal
360 }
361
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000362
363# Top level object so multiprocessing can pickle
364# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000365class _PresubmitError(_PresubmitResult):
366 """A hard presubmit error."""
367 fatal = True
368
369
370# Top level object so multiprocessing can pickle
371# Public access through OutputApi object.
372class _PresubmitPromptWarning(_PresubmitResult):
373 """An warning that prompts the user if they want to continue."""
374 should_prompt = True
375
376
377# Top level object so multiprocessing can pickle
378# Public access through OutputApi object.
379class _PresubmitNotifyResult(_PresubmitResult):
380 """Just print something to the screen -- but it's not even a warning."""
381 pass
382
383
384# Top level object so multiprocessing can pickle
385# Public access through OutputApi object.
386class _MailTextResult(_PresubmitResult):
387 """A warning that should be included in the review request email."""
388 def __init__(self, *args, **kwargs):
389 super(_MailTextResult, self).__init__()
390 raise NotImplementedError()
391
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000392class GerritAccessor(object):
393 """Limited Gerrit functionality for canned presubmit checks to work.
394
395 To avoid excessive Gerrit calls, caches the results.
396 """
397
Edward Lesmeseb1bd622021-03-01 19:54:07 +0000398 def __init__(self, url=None, project=None, branch=None):
399 self.host = urlparse.urlparse(url).netloc if url else None
400 self.project = project
401 self.branch = branch
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000402 self.cache = {}
Edward Lesmes8170c292021-03-19 20:04:43 +0000403 self.code_owners_enabled = None
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000404
405 def _FetchChangeDetail(self, issue):
406 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100407 try:
408 return gerrit_util.GetChangeDetail(
409 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700410 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100411 except gerrit_util.GerritError as e:
412 if e.http_status == 404:
413 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
414 'no credentials to fetch issue details' % issue)
415 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000416
417 def GetChangeInfo(self, issue):
418 """Returns labels and all revisions (patchsets) for this issue.
419
420 The result is a dictionary according to Gerrit REST Api.
421 https://gerrit-review.googlesource.com/Documentation/rest-api.html
422
423 However, API isn't very clear what's inside, so see tests for example.
424 """
425 assert issue
426 cache_key = int(issue)
427 if cache_key not in self.cache:
428 self.cache[cache_key] = self._FetchChangeDetail(issue)
429 return self.cache[cache_key]
430
431 def GetChangeDescription(self, issue, patchset=None):
432 """If patchset is none, fetches current patchset."""
433 info = self.GetChangeInfo(issue)
434 # info is a reference to cache. We'll modify it here adding description to
435 # it to the right patchset, if it is not yet there.
436
437 # Find revision info for the patchset we want.
438 if patchset is not None:
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000439 for rev, rev_info in info['revisions'].items():
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000440 if str(rev_info['_number']) == str(patchset):
441 break
442 else:
443 raise Exception('patchset %s doesn\'t exist in issue %s' % (
444 patchset, issue))
445 else:
446 rev = info['current_revision']
447 rev_info = info['revisions'][rev]
448
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100449 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000450
Mun Yong Jang603d01e2017-12-19 16:38:30 -0800451 def GetDestRef(self, issue):
452 ref = self.GetChangeInfo(issue)['branch']
453 if not ref.startswith('refs/'):
454 # NOTE: it is possible to create 'refs/x' branch,
455 # aka 'refs/heads/refs/x'. However, this is ill-advised.
456 ref = 'refs/heads/%s' % ref
457 return ref
458
Edward Lesmes02d4b822020-11-11 00:37:35 +0000459 def _GetApproversForLabel(self, issue, label):
460 change_info = self.GetChangeInfo(issue)
461 label_info = change_info.get('labels', {}).get(label, {})
462 values = label_info.get('values', {}).keys()
463 if not values:
464 return []
465 max_value = max(int(v) for v in values)
466 return [v for v in label_info.get('all', [])
467 if v.get('value', 0) == max_value]
468
Edward Lesmesc4566172021-03-19 16:55:13 +0000469 def IsBotCommitApproved(self, issue):
470 return bool(self._GetApproversForLabel(issue, 'Bot-Commit'))
471
Edward Lesmescf49cb82020-11-11 01:08:36 +0000472 def IsOwnersOverrideApproved(self, issue):
473 return bool(self._GetApproversForLabel(issue, 'Owners-Override'))
474
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000475 def GetChangeOwner(self, issue):
476 return self.GetChangeInfo(issue)['owner']['email']
477
478 def GetChangeReviewers(self, issue, approving_only=True):
Aaron Gable8b478f02017-07-31 15:33:19 -0700479 changeinfo = self.GetChangeInfo(issue)
480 if approving_only:
Edward Lesmes02d4b822020-11-11 00:37:35 +0000481 reviewers = self._GetApproversForLabel(issue, 'Code-Review')
Aaron Gable8b478f02017-07-31 15:33:19 -0700482 else:
483 reviewers = changeinfo.get('reviewers', {}).get('REVIEWER', [])
484 return [r.get('email') for r in reviewers]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000485
Edward Lemure4d329c2020-02-03 20:41:18 +0000486 def UpdateDescription(self, description, issue):
487 gerrit_util.SetCommitMessage(self.host, issue, description, notify='NONE')
488
Edward Lesmes8170c292021-03-19 20:04:43 +0000489 def IsCodeOwnersEnabledOnRepo(self):
490 if self.code_owners_enabled is None:
491 self.code_owners_enabled = gerrit_util.IsCodeOwnersEnabledOnRepo(
Edward Lesmes392c4072021-03-19 21:58:45 +0000492 self.host, self.project)
Edward Lesmes8170c292021-03-19 20:04:43 +0000493 return self.code_owners_enabled
494
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000495
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000496class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000497 """An instance of OutputApi gets passed to presubmit scripts so that they
498 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000499 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000500 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000501 PresubmitError = _PresubmitError
502 PresubmitPromptWarning = _PresubmitPromptWarning
503 PresubmitNotifyResult = _PresubmitNotifyResult
504 MailTextResult = _MailTextResult
505
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000506 def __init__(self, is_committing):
507 self.is_committing = is_committing
Daniel Cheng7227d212017-11-17 08:12:37 -0800508 self.more_cc = []
509
510 def AppendCC(self, cc):
511 """Appends a user to cc for this change."""
512 self.more_cc.append(cc)
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000513
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000514 def PresubmitPromptOrNotify(self, *args, **kwargs):
515 """Warn the user when uploading, but only notify if committing."""
516 if self.is_committing:
517 return self.PresubmitNotifyResult(*args, **kwargs)
518 return self.PresubmitPromptWarning(*args, **kwargs)
519
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000520
521class InputApi(object):
522 """An instance of this object is passed to presubmit scripts so they can
523 know stuff about the change they're looking at.
524 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000525 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800526 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000527
maruel@chromium.org3410d912009-06-09 20:56:16 +0000528 # File extensions that are considered source files from a style guide
529 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000530 #
531 # Files without an extension aren't included in the list. If you want to
local_bot30f774e2020-06-25 18:23:34 +0000532 # filter them as source files, add r'(^|.*?[\\\/])[^.]+$' to the allow list.
local_bot64021412020-07-08 21:05:39 +0000533 # Note that ALL CAPS files are skipped in DEFAULT_FILES_TO_SKIP below.
534 DEFAULT_FILES_TO_CHECK = (
maruel@chromium.org3410d912009-06-09 20:56:16 +0000535 # C++ and friends
Edward Lemura5799e32020-01-17 19:26:51 +0000536 r'.+\.c$', r'.+\.cc$', r'.+\.cpp$', r'.+\.h$', r'.+\.m$', r'.+\.mm$',
537 r'.+\.inl$', r'.+\.asm$', r'.+\.hxx$', r'.+\.hpp$', r'.+\.s$', r'.+\.S$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000538 # Scripts
Edward Lemura5799e32020-01-17 19:26:51 +0000539 r'.+\.js$', r'.+\.py$', r'.+\.sh$', r'.+\.rb$', r'.+\.pl$', r'.+\.pm$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000540 # Other
Edward Lemura5799e32020-01-17 19:26:51 +0000541 r'.+\.java$', r'.+\.mk$', r'.+\.am$', r'.+\.css$', r'.+\.mojom$',
542 r'.+\.fidl$'
maruel@chromium.org3410d912009-06-09 20:56:16 +0000543 )
544
545 # Path regexp that should be excluded from being considered containing source
546 # files. Don't modify this list from a presubmit script!
local_bot64021412020-07-08 21:05:39 +0000547 DEFAULT_FILES_TO_SKIP = (
Edward Lemura5799e32020-01-17 19:26:51 +0000548 r'testing_support[\\\/]google_appengine[\\\/].*',
549 r'.*\bexperimental[\\\/].*',
Kent Tamura179dd1e2018-04-26 15:07:41 +0900550 # Exclude third_party/.* but NOT third_party/{WebKit,blink}
551 # (crbug.com/539768 and crbug.com/836555).
Edward Lemura5799e32020-01-17 19:26:51 +0000552 r'.*\bthird_party[\\\/](?!(WebKit|blink)[\\\/]).*',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000553 # Output directories (just in case)
Edward Lemura5799e32020-01-17 19:26:51 +0000554 r'.*\bDebug[\\\/].*',
555 r'.*\bRelease[\\\/].*',
556 r'.*\bxcodebuild[\\\/].*',
557 r'.*\bout[\\\/].*',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000558 # All caps files like README and LICENCE.
Edward Lemura5799e32020-01-17 19:26:51 +0000559 r'.*\b[A-Z0-9_]{2,}$',
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000560 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
Edward Lemura5799e32020-01-17 19:26:51 +0000561 r'(|.*[\\\/])\.git[\\\/].*',
562 r'(|.*[\\\/])\.svn[\\\/].*',
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000563 # There is no point in processing a patch file.
Edward Lemura5799e32020-01-17 19:26:51 +0000564 r'.+\.diff$',
565 r'.+\.patch$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000566 )
567
local_bot30f774e2020-06-25 18:23:34 +0000568 # TODO(https://crbug.com/1098562): Remove once no longer used
569 @property
570 def DEFAULT_WHITE_LIST(self):
local_bot64021412020-07-08 21:05:39 +0000571 return self.DEFAULT_FILES_TO_CHECK
local_bot30f774e2020-06-25 18:23:34 +0000572
573 # TODO(https://crbug.com/1098562): Remove once no longer used
local_bot37ce2012020-06-26 17:39:24 +0000574 @DEFAULT_WHITE_LIST.setter
575 def DEFAULT_WHITE_LIST(self, value):
local_bot64021412020-07-08 21:05:39 +0000576 self.DEFAULT_FILES_TO_CHECK = value
577
578 # TODO(https://crbug.com/1098562): Remove once no longer used
579 @property
580 def DEFAULT_ALLOW_LIST(self):
581 return self.DEFAULT_FILES_TO_CHECK
582
583 # TODO(https://crbug.com/1098562): Remove once no longer used
584 @DEFAULT_ALLOW_LIST.setter
585 def DEFAULT_ALLOW_LIST(self, value):
586 self.DEFAULT_FILES_TO_CHECK = value
local_bot37ce2012020-06-26 17:39:24 +0000587
588 # TODO(https://crbug.com/1098562): Remove once no longer used
local_bot30f774e2020-06-25 18:23:34 +0000589 @property
590 def DEFAULT_BLACK_LIST(self):
local_bot64021412020-07-08 21:05:39 +0000591 return self.DEFAULT_FILES_TO_SKIP
local_bot30f774e2020-06-25 18:23:34 +0000592
local_bot37ce2012020-06-26 17:39:24 +0000593 # TODO(https://crbug.com/1098562): Remove once no longer used
594 @DEFAULT_BLACK_LIST.setter
595 def DEFAULT_BLACK_LIST(self, value):
local_bot64021412020-07-08 21:05:39 +0000596 self.DEFAULT_FILES_TO_SKIP = value
597
598 # TODO(https://crbug.com/1098562): Remove once no longer used
599 @property
600 def DEFAULT_BLOCK_LIST(self):
601 return self.DEFAULT_FILES_TO_SKIP
602
603 # TODO(https://crbug.com/1098562): Remove once no longer used
604 @DEFAULT_BLOCK_LIST.setter
605 def DEFAULT_BLOCK_LIST(self, value):
606 self.DEFAULT_FILES_TO_SKIP = value
local_bot37ce2012020-06-26 17:39:24 +0000607
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000608 def __init__(self, change, presubmit_path, is_committing,
Edward Lesmes8e282792018-04-03 18:50:29 -0400609 verbose, gerrit_obj, dry_run=None, thread_pool=None, parallel=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000610 """Builds an InputApi object.
611
612 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000613 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000614 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000615 is_committing: True if the change is about to be committed.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000616 gerrit_obj: provides basic Gerrit codereview functionality.
617 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -0400618 parallel: if true, all tests reported via input_api.RunTests for all
619 PRESUBMIT files will be run in parallel.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000620 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000621 # Version number of the presubmit_support script.
622 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000623 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000624 self.is_committing = is_committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000625 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000626 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000627
Edward Lesmes8e282792018-04-03 18:50:29 -0400628 self.parallel = parallel
629 self.thread_pool = thread_pool or ThreadPool()
630
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000631 # We expose various modules and functions as attributes of the input_api
632 # so that presubmit scripts don't have to import them.
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +0900633 self.ast = ast
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000634 self.basename = os.path.basename
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000635 self.cpplint = cpplint
dcheng091b7db2016-06-16 01:27:51 -0700636 self.fnmatch = fnmatch
Yoshisato Yanagisawa04600b42019-03-15 03:03:41 +0000637 self.gclient_paths = gclient_paths
Yoshisato Yanagisawa57dd17b2019-03-22 09:10:29 +0000638 # TODO(yyanagisawa): stop exposing this when python3 become default.
639 # Since python3's tempfile has TemporaryDirectory, we do not need this.
640 self.temporary_directory = gclient_utils.temporary_directory
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000641 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000642 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000643 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000644 self.os_listdir = os.listdir
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000645 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000646 self.os_stat = os.stat
Yoshisato Yanagisawa406de132018-06-29 05:43:25 +0000647 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000648 self.re = re
649 self.subprocess = subprocess
Edward Lemurb9830242019-10-30 22:19:20 +0000650 self.sys = sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000651 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000652 self.time = time
maruel@chromium.org1487d532009-06-06 00:22:57 +0000653 self.unittest = unittest
Edward Lemurb9830242019-10-30 22:19:20 +0000654 if sys.version_info.major == 2:
655 self.urllib2 = urllib2
Edward Lemur16af3562019-10-17 22:11:33 +0000656 self.urllib_request = urllib_request
657 self.urllib_error = urllib_error
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000658
Robert Iannucci50258932018-03-19 10:30:59 -0700659 self.is_windows = sys.platform == 'win32'
660
Edward Lemurb9646622019-10-25 20:57:35 +0000661 # Set python_executable to 'vpython' in order to allow scripts in other
662 # repos (e.g. src.git) to automatically pick up that repo's .vpython file,
663 # instead of inheriting the one in depot_tools.
664 self.python_executable = 'vpython'
Erik Staab69135d12021-05-14 22:31:57 +0000665 # Offer a python 3 executable for use during the migration off of python 2.
666 self.python3_executable = 'vpython3'
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000667 self.environ = os.environ
668
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000669 # InputApi.platform is the platform you're currently running on.
670 self.platform = sys.platform
671
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000672 self.cpu_count = multiprocessing.cpu_count()
673
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000674 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000675 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000676
677 # We carry the canned checks so presubmit scripts can easily use them.
678 self.canned_checks = presubmit_canned_checks
679
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100680 # Temporary files we must manually remove at the end of a run.
681 self._named_temporary_files = []
Jochen Eisinger72606f82017-04-04 10:44:18 +0200682
Edward Lesmesdc11c6b2021-03-19 19:22:05 +0000683 self.owners_client = None
684 if self.gerrit:
685 self.owners_client = owners_client.GetCodeOwnersClient(
686 root=change.RepositoryRoot(),
687 upstream=change.UpstreamBranch(),
688 host=self.gerrit.host,
689 project=self.gerrit.project,
690 branch=self.gerrit.branch)
Edward Lesmes9ce03f82021-01-12 20:13:31 +0000691 self.owners_db = owners_db.Database(
692 change.RepositoryRoot(), fopen=open, os_path=self.os_path)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200693 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000694 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000695 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000696
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000697 # Replace <hash_map> and <hash_set> as headers that need to be included
Edward Lemura5799e32020-01-17 19:26:51 +0000698 # with 'base/containers/hash_tables.h' instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000699 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800700 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000701 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000702 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000703 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
704 for (a, b, header) in cpplint._re_pattern_templates
705 ]
706
Edward Lemurecc27072020-01-06 16:42:34 +0000707 def SetTimeout(self, timeout):
708 self.thread_pool.timeout = timeout
709
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000710 def PresubmitLocalPath(self):
711 """Returns the local path of the presubmit script currently being run.
712
713 This is useful if you don't want to hard-code absolute paths in the
714 presubmit script. For example, It can be used to find another file
715 relative to the PRESUBMIT.py script, so the whole tree can be branched and
716 the presubmit script still works, without editing its content.
717 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000718 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000719
agable0b65e732016-11-22 09:25:46 -0800720 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000721 """Same as input_api.change.AffectedFiles() except only lists files
722 (and optionally directories) in the same directory as the current presubmit
Bruce Dawson7a0b07a2020-04-23 17:14:40 +0000723 script, or subdirectories thereof. Note that files are listed using the OS
724 path separator, so backslashes are used as separators on Windows.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000725 """
Edward Lemura5799e32020-01-17 19:26:51 +0000726 dir_with_slash = normpath('%s/' % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000727 if len(dir_with_slash) == 1:
728 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000729
Edward Lemurb9830242019-10-30 22:19:20 +0000730 return list(filter(
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000731 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
Edward Lemurb9830242019-10-30 22:19:20 +0000732 self.change.AffectedFiles(include_deletes, file_filter)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000733
agable0b65e732016-11-22 09:25:46 -0800734 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000735 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800736 paths = [af.LocalPath() for af in self.AffectedFiles()]
Edward Lemura5799e32020-01-17 19:26:51 +0000737 logging.debug('LocalPaths: %s', paths)
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000738 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000739
agable0b65e732016-11-22 09:25:46 -0800740 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000741 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800742 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000743
John Budorick16162372018-04-18 10:39:53 -0700744 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
agable0b65e732016-11-22 09:25:46 -0800745 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000746 in the same directory as the current presubmit script, or subdirectories
747 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000748 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000749 if include_deletes is not None:
Edward Lemura5799e32020-01-17 19:26:51 +0000750 warn('AffectedTestableFiles(include_deletes=%s)'
751 ' is deprecated and ignored' % str(include_deletes),
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000752 category=DeprecationWarning,
753 stacklevel=2)
Edward Lemurb9830242019-10-30 22:19:20 +0000754 return list(filter(
755 lambda x: x.IsTestableFile(),
756 self.AffectedFiles(include_deletes=False, **kwargs)))
agable0b65e732016-11-22 09:25:46 -0800757
758 def AffectedTextFiles(self, include_deletes=None):
759 """An alias to AffectedTestableFiles for backwards compatibility."""
760 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000761
Josip Sokcevic8c955952021-02-01 21:32:57 +0000762 def FilterSourceFile(self,
763 affected_file,
764 files_to_check=None,
765 files_to_skip=None,
766 allow_list=None,
767 block_list=None):
Edward Lemura5799e32020-01-17 19:26:51 +0000768 """Filters out files that aren't considered 'source file'.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000769
local_bot64021412020-07-08 21:05:39 +0000770 If files_to_check or files_to_skip is None, InputApi.DEFAULT_FILES_TO_CHECK
771 and InputApi.DEFAULT_FILES_TO_SKIP is used respectively.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000772
773 The lists will be compiled as regular expression and
774 AffectedFile.LocalPath() needs to pass both list.
775
776 Note: Copy-paste this function to suit your needs or use a lambda function.
777 """
local_bot64021412020-07-08 21:05:39 +0000778 if files_to_check is None:
779 files_to_check = self.DEFAULT_FILES_TO_CHECK
780 if files_to_skip is None:
781 files_to_skip = self.DEFAULT_FILES_TO_SKIP
local_bot30f774e2020-06-25 18:23:34 +0000782
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000783 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000784 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000785 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000786 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000787 return True
788 return False
local_bot64021412020-07-08 21:05:39 +0000789 return (Find(affected_file, files_to_check) and
790 not Find(affected_file, files_to_skip))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000791
792 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800793 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000794
795 If source_file is None, InputApi.FilterSourceFile() is used.
796 """
797 if not source_file:
798 source_file = self.FilterSourceFile
Edward Lemurb9830242019-10-30 22:19:20 +0000799 return list(filter(source_file, self.AffectedTestableFiles()))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000800
801 def RightHandSideLines(self, source_file_filter=None):
Edward Lemura5799e32020-01-17 19:26:51 +0000802 """An iterator over all text lines in 'new' version of changed files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000803
804 Only lists lines from new or modified text files in the change that are
805 contained by the directory of the currently executing presubmit script.
806
807 This is useful for doing line-by-line regex checks, like checking for
808 trailing whitespace.
809
810 Yields:
811 a 3 tuple:
812 the AffectedFile instance of the current file;
813 integer line number (1-based); and
814 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000815
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000816 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000817 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000818 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000819 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000820
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000821 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000822 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000823
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000824 Deny reading anything outside the repository.
825 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000826 if isinstance(file_item, AffectedFile):
827 file_item = file_item.AbsoluteLocalPath()
828 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000829 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000830 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000831
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100832 def CreateTemporaryFile(self, **kwargs):
833 """Returns a named temporary file that must be removed with a call to
834 RemoveTemporaryFiles().
835
836 All keyword arguments are forwarded to tempfile.NamedTemporaryFile(),
837 except for |delete|, which is always set to False.
838
839 Presubmit checks that need to create a temporary file and pass it for
840 reading should use this function instead of NamedTemporaryFile(), as
841 Windows fails to open a file that is already open for writing.
842
843 with input_api.CreateTemporaryFile() as f:
844 f.write('xyz')
845 f.close()
846 input_api.subprocess.check_output(['script-that', '--reads-from',
847 f.name])
848
849
850 Note that callers of CreateTemporaryFile() should not worry about removing
851 any temporary file; this is done transparently by the presubmit handling
852 code.
853 """
854 if 'delete' in kwargs:
855 # Prevent users from passing |delete|; we take care of file deletion
856 # ourselves and this prevents unintuitive error messages when we pass
857 # delete=False and 'delete' is also in kwargs.
858 raise TypeError('CreateTemporaryFile() does not take a "delete" '
859 'argument, file deletion is handled automatically by '
860 'the same presubmit_support code that creates InputApi '
861 'objects.')
862 temp_file = self.tempfile.NamedTemporaryFile(delete=False, **kwargs)
863 self._named_temporary_files.append(temp_file.name)
864 return temp_file
865
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000866 @property
867 def tbr(self):
868 """Returns if a change is TBR'ed."""
Jeremy Romandce22502017-06-20 15:37:29 -0400869 return 'TBR' in self.change.tags or self.change.TBRsFromDescription()
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000870
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000871 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000872 tests = []
873 msgs = []
874 for t in tests_mix:
Edward Lesmes8e282792018-04-03 18:50:29 -0400875 if isinstance(t, OutputApi.PresubmitResult) and t:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000876 msgs.append(t)
877 else:
878 assert issubclass(t.message, _PresubmitResult)
879 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000880 if self.verbose:
881 t.info = _PresubmitNotifyResult
Edward Lemur1037c742018-05-01 18:56:04 -0400882 if not t.kwargs.get('cwd'):
883 t.kwargs['cwd'] = self.PresubmitLocalPath()
Edward Lesmes8e282792018-04-03 18:50:29 -0400884 self.thread_pool.AddTests(tests, parallel)
Edward Lemur21000eb2019-05-24 23:25:58 +0000885 # When self.parallel is True (i.e. --parallel is passed as an option)
886 # RunTests doesn't actually run tests. It adds them to a ThreadPool that
887 # will run all tests once all PRESUBMIT files are processed.
888 # Otherwise, it will run them and return the results.
889 if not self.parallel:
Edward Lesmes8e282792018-04-03 18:50:29 -0400890 msgs.extend(self.thread_pool.RunAsync())
891 return msgs
scottmg86099d72016-09-01 09:16:51 -0700892
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000893
nick@chromium.orgff526192013-06-10 19:30:26 +0000894class _DiffCache(object):
895 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000896 def __init__(self, upstream=None):
897 """Stores the upstream revision against which all diffs will be computed."""
898 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000899
900 def GetDiff(self, path, local_root):
901 """Get the diff for a particular path."""
902 raise NotImplementedError()
903
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700904 def GetOldContents(self, path, local_root):
905 """Get the old version for a particular path."""
906 raise NotImplementedError()
907
nick@chromium.orgff526192013-06-10 19:30:26 +0000908
nick@chromium.orgff526192013-06-10 19:30:26 +0000909class _GitDiffCache(_DiffCache):
910 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000911 def __init__(self, upstream):
912 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000913 self._diffs_by_file = None
914
915 def GetDiff(self, path, local_root):
916 if not self._diffs_by_file:
917 # Compute a single diff for all files and parse the output; should
918 # with git this is much faster than computing one diff for each file.
919 diffs = {}
920
921 # Don't specify any filenames below, because there are command line length
922 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000923 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
924 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000925
926 # This regex matches the path twice, separated by a space. Note that
927 # filename itself may contain spaces.
928 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
929 current_diff = []
930 keep_line_endings = True
931 for x in unified_diff.splitlines(keep_line_endings):
932 match = file_marker.match(x)
933 if match:
934 # Marks the start of a new per-file section.
935 diffs[match.group('filename')] = current_diff = [x]
936 elif x.startswith('diff --git'):
937 raise PresubmitFailure('Unexpected diff line: %s' % x)
938 else:
939 current_diff.append(x)
940
941 self._diffs_by_file = dict(
942 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
943
944 if path not in self._diffs_by_file:
945 raise PresubmitFailure(
946 'Unified diff did not contain entry for file %s' % path)
947
948 return self._diffs_by_file[path]
949
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700950 def GetOldContents(self, path, local_root):
951 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
952
nick@chromium.orgff526192013-06-10 19:30:26 +0000953
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000954class AffectedFile(object):
955 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000956
957 DIFF_CACHE = _DiffCache
958
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000959 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800960 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000961 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000962 self._path = path
963 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000964 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000965 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000966 self._cached_changed_contents = None
967 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000968 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700969 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000970
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000971 def LocalPath(self):
972 """Returns the path of this file on the local disk relative to client root.
Andrew Grieve92b8b992017-11-02 09:42:24 -0400973
974 This should be used for error messages but not for accessing files,
975 because presubmit checks are run with CWD=PresubmitLocalPath() (which is
976 often != client root).
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000977 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000978 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000979
980 def AbsoluteLocalPath(self):
981 """Returns the absolute path of this file on the local disk.
982 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000983 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000985 def Action(self):
986 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000987 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000988
agable0b65e732016-11-22 09:25:46 -0800989 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000990 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000991
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000992 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000993 raise NotImplementedError() # Implement when needed
994
agable0b65e732016-11-22 09:25:46 -0800995 def IsTextFile(self):
996 """An alias to IsTestableFile for backwards compatibility."""
997 return self.IsTestableFile()
998
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700999 def OldContents(self):
1000 """Returns an iterator over the lines in the old version of file.
1001
Daniel Cheng2da34fe2017-03-21 20:42:12 -07001002 The old version is the file before any modifications in the user's
Edward Lemura5799e32020-01-17 19:26:51 +00001003 workspace, i.e. the 'left hand side'.
Daniel Cheng7a1f04d2017-03-21 19:12:31 -07001004
1005 Contents will be empty if the file is a directory or does not exist.
1006 Note: The carriage returns (LF or CR) are stripped off.
1007 """
1008 return self._diff_cache.GetOldContents(self.LocalPath(),
1009 self._local_root).splitlines()
1010
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001011 def NewContents(self):
1012 """Returns an iterator over the lines in the new version of file.
1013
Edward Lemura5799e32020-01-17 19:26:51 +00001014 The new version is the file in the user's workspace, i.e. the 'right hand
1015 side'.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001016
1017 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +00001018 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001019 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +00001020 if self._cached_new_contents is None:
1021 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -08001022 try:
1023 self._cached_new_contents = gclient_utils.FileRead(
1024 self.AbsoluteLocalPath(), 'rU').splitlines()
1025 except IOError:
1026 pass # File not found? That's fine; maybe it was deleted.
Greg Thompson30cde452021-06-01 16:38:47 +00001027 except UnicodeDecodeError as e:
Dirk Pranke6fc394f2021-05-26 23:25:14 +00001028 # log the filename since we're probably trying to read a binary
1029 # file, and shouldn't be.
1030 print('Error reading %s: %s' % (self.AbsoluteLocalPath(), e))
1031 raise
1032
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +00001033 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001034
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001035 def ChangedContents(self, keeplinebreaks=False):
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001036 """Returns a list of tuples (line number, line text) of all new lines.
1037
1038 This relies on the scm diff output describing each changed code section
1039 with a line of the form
1040
1041 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
1042 """
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001043 # Don't return cached results when line breaks are requested.
1044 if not keeplinebreaks and self._cached_changed_contents is not None:
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +00001045 return self._cached_changed_contents[:]
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001046 result = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001047 line_num = 0
1048
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001049 # The keeplinebreaks parameter to splitlines must be True or else the
1050 # CheckForWindowsLineEndings presubmit will be a NOP.
1051 for line in self.GenerateScmDiff().splitlines(keeplinebreaks):
Edward Lemurac5c55f2020-02-29 00:17:16 +00001052 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
1053 if m:
1054 line_num = int(m.groups(1)[0])
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001055 continue
1056 if line.startswith('+') and not line.startswith('++'):
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001057 result.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001058 if not line.startswith('-'):
1059 line_num += 1
Bruce Dawsonf0bcfdd2021-05-21 18:10:23 +00001060 # Don't cache results with line breaks.
1061 if keeplinebreaks:
1062 return result;
1063 self._cached_changed_contents = result
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +00001064 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001065
maruel@chromium.org5de13972009-06-10 18:16:06 +00001066 def __str__(self):
1067 return self.LocalPath()
1068
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001069 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +00001070 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001071
maruel@chromium.org58407af2011-04-12 23:15:57 +00001072
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001073class GitAffectedFile(AffectedFile):
1074 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +00001075 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001076 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001077
nick@chromium.orgff526192013-06-10 19:30:26 +00001078 DIFF_CACHE = _GitDiffCache
1079
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001080 def __init__(self, *args, **kwargs):
1081 AffectedFile.__init__(self, *args, **kwargs)
1082 self._server_path = None
agable0b65e732016-11-22 09:25:46 -08001083 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001084
agable0b65e732016-11-22 09:25:46 -08001085 def IsTestableFile(self):
1086 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001087 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -08001088 # A deleted file is not testable.
1089 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001090 else:
agable0b65e732016-11-22 09:25:46 -08001091 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
1092 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001093
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001094
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001095class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001096 """Describe a change.
1097
1098 Used directly by the presubmit scripts to query the current change being
1099 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001100
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001101 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +00001102 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001103 self.KEY: equivalent to tags['KEY']
1104 """
1105
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001106 _AFFECTED_FILES = AffectedFile
1107
Edward Lemura5799e32020-01-17 19:26:51 +00001108 # Matches key/value (or 'tag') lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +00001109 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001110 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001111 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001112
maruel@chromium.org58407af2011-04-12 23:15:57 +00001113 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001114 self, name, description, local_root, files, issue, patchset, author,
1115 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001116 if files is None:
1117 files = []
1118 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +00001119 # Convert root into an absolute path.
1120 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001121 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001122 self.issue = issue
1123 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +00001124 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001125
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001126 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001127 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001128 self._description_without_tags = ''
1129 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001130
maruel@chromium.orge085d812011-10-10 19:49:15 +00001131 assert all(
1132 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
1133
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001134 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001135 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +00001136 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
1137 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +00001138 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001139
Edward Lesmes9ce03f82021-01-12 20:13:31 +00001140 def UpstreamBranch(self):
1141 """Returns the upstream branch for the change."""
1142 return self._upstream
1143
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001144 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001145 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001146 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001147
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001148 def DescriptionText(self):
1149 """Returns the user-entered changelist description, minus tags.
1150
Edward Lemura5799e32020-01-17 19:26:51 +00001151 Any line in the user-provided description starting with e.g. 'FOO='
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001152 (whitespace permitted before and around) is considered a tag line. Such
1153 lines are stripped out of the description this function returns.
1154 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001155 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001156
1157 def FullDescriptionText(self):
1158 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001159 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001160
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001161 def SetDescriptionText(self, description):
1162 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001163
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001164 Also updates the list of tags."""
1165 self._full_description = description
1166
1167 # From the description text, build up a dictionary of key/value pairs
Edward Lemura5799e32020-01-17 19:26:51 +00001168 # plus the description minus all key/value or 'tag' lines.
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001169 description_without_tags = []
1170 self.tags = {}
1171 for line in self._full_description.splitlines():
1172 m = self.TAG_LINE_RE.match(line)
1173 if m:
1174 self.tags[m.group('key')] = m.group('value')
1175 else:
1176 description_without_tags.append(line)
1177
1178 # Change back to text and remove whitespace at end.
1179 self._description_without_tags = (
1180 '\n'.join(description_without_tags).rstrip())
1181
Edward Lemur69bb8be2020-02-03 20:37:38 +00001182 def AddDescriptionFooter(self, key, value):
1183 """Adds the given footer to the change description.
1184
1185 Args:
1186 key: A string with the key for the git footer. It must conform to
1187 the git footers format (i.e. 'List-Of-Tokens') and will be case
1188 normalized so that each token is title-cased.
1189 value: A string with the value for the git footer.
1190 """
1191 description = git_footers.add_footer(
1192 self.FullDescriptionText(), git_footers.normalize_name(key), value)
1193 self.SetDescriptionText(description)
1194
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001195 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001196 """Returns the repository (checkout) root directory for this change,
1197 as an absolute path.
1198 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001199 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001200
1201 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001202 """Return tags directly as attributes on the object."""
Edward Lemura5799e32020-01-17 19:26:51 +00001203 if not re.match(r'^[A-Z_]*$', attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001204 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +00001205 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001206
Edward Lemur69bb8be2020-02-03 20:37:38 +00001207 def GitFootersFromDescription(self):
1208 """Return the git footers present in the description.
1209
1210 Returns:
1211 footers: A dict of {footer: [values]} containing a multimap of the footers
1212 in the change description.
1213 """
1214 return git_footers.parse_footers(self.FullDescriptionText())
1215
Aaron Gablefc03e672017-05-15 14:09:42 -07001216 def BugsFromDescription(self):
1217 """Returns all bugs referenced in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -07001218 tags = [b.strip() for b in self.tags.get('BUG', '').split(',') if b.strip()]
Caleb Rouleauc0546b92019-02-22 06:12:57 +00001219 footers = []
Edward Lemur69bb8be2020-02-03 20:37:38 +00001220 parsed = self.GitFootersFromDescription()
Dan Beam62954042019-10-03 21:20:33 +00001221 unsplit_footers = parsed.get('Bug', []) + parsed.get('Fixed', [])
Caleb Rouleauc0546b92019-02-22 06:12:57 +00001222 for unsplit_footer in unsplit_footers:
1223 footers += [b.strip() for b in unsplit_footer.split(',')]
Aaron Gable12ef5012017-05-15 14:29:00 -07001224 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -07001225
1226 def ReviewersFromDescription(self):
1227 """Returns all reviewers listed in the commit description."""
Edward Lemura5799e32020-01-17 19:26:51 +00001228 # We don't support a 'R:' git-footer for reviewers; that is in metadata.
Aaron Gable12ef5012017-05-15 14:29:00 -07001229 tags = [r.strip() for r in self.tags.get('R', '').split(',') if r.strip()]
1230 return sorted(set(tags))
Aaron Gablefc03e672017-05-15 14:09:42 -07001231
1232 def TBRsFromDescription(self):
1233 """Returns all TBR reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -07001234 tags = [r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()]
Aaron Gable6e7ddb62020-05-27 22:23:29 +00001235 # TODO(crbug.com/839208): Remove support for 'Tbr:' when TBRs are
1236 # programmatically determined by self-CR+1s.
Edward Lemur69bb8be2020-02-03 20:37:38 +00001237 footers = self.GitFootersFromDescription().get('Tbr', [])
Aaron Gable12ef5012017-05-15 14:29:00 -07001238 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -07001239
Aaron Gable6e7ddb62020-05-27 22:23:29 +00001240 # TODO(crbug.com/753425): Delete these once we're sure they're unused.
Aaron Gablefc03e672017-05-15 14:09:42 -07001241 @property
1242 def BUG(self):
1243 return ','.join(self.BugsFromDescription())
1244 @property
1245 def R(self):
1246 return ','.join(self.ReviewersFromDescription())
1247 @property
1248 def TBR(self):
1249 return ','.join(self.TBRsFromDescription())
1250
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001251 def AllFiles(self, root=None):
1252 """List all files under source control in the repo."""
1253 raise NotImplementedError()
1254
agable0b65e732016-11-22 09:25:46 -08001255 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001256 """Returns a list of AffectedFile instances for all files in the change.
1257
1258 Args:
1259 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +00001260 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001261
1262 Returns:
1263 [AffectedFile(path, action), AffectedFile(path, action)]
1264 """
Edward Lemurb9830242019-10-30 22:19:20 +00001265 affected = list(filter(file_filter, self._affected_files))
sail@chromium.org5538e022011-05-12 17:53:16 +00001266
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001267 if include_deletes:
1268 return affected
Edward Lemurb9830242019-10-30 22:19:20 +00001269 return list(filter(lambda x: x.Action() != 'D', affected))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001270
John Budorick16162372018-04-18 10:39:53 -07001271 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +00001272 """Return a list of the existing text files in a change."""
1273 if include_deletes is not None:
Edward Lemura5799e32020-01-17 19:26:51 +00001274 warn('AffectedTeestableFiles(include_deletes=%s)'
1275 ' is deprecated and ignored' % str(include_deletes),
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001276 category=DeprecationWarning,
1277 stacklevel=2)
Edward Lemurb9830242019-10-30 22:19:20 +00001278 return list(filter(
1279 lambda x: x.IsTestableFile(),
1280 self.AffectedFiles(include_deletes=False, **kwargs)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001281
agable0b65e732016-11-22 09:25:46 -08001282 def AffectedTextFiles(self, include_deletes=None):
1283 """An alias to AffectedTestableFiles for backwards compatibility."""
1284 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001285
agable0b65e732016-11-22 09:25:46 -08001286 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001287 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001288 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001289
agable0b65e732016-11-22 09:25:46 -08001290 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001291 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001292 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001293
1294 def RightHandSideLines(self):
Edward Lemura5799e32020-01-17 19:26:51 +00001295 """An iterator over all text lines in 'new' version of changed files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001296
1297 Lists lines from new or modified text files in the change.
1298
1299 This is useful for doing line-by-line regex checks, like checking for
1300 trailing whitespace.
1301
1302 Yields:
1303 a 3 tuple:
1304 the AffectedFile instance of the current file;
1305 integer line number (1-based); and
1306 the contents of the line as a string.
1307 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001308 return _RightHandSideLinesImpl(
1309 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -08001310 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001311
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02001312 def OriginalOwnersFiles(self):
1313 """A map from path names of affected OWNERS files to their old content."""
1314 def owners_file_filter(f):
1315 return 'OWNERS' in os.path.split(f.LocalPath())[1]
1316 files = self.AffectedFiles(file_filter=owners_file_filter)
1317 return dict([(f.LocalPath(), f.OldContents()) for f in files])
1318
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001319
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001320class GitChange(Change):
1321 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001322 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001323
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001324 def AllFiles(self, root=None):
1325 """List all files under source control in the repo."""
1326 root = root or self.RepositoryRoot()
1327 return subprocess.check_output(
Aaron Gable7817f022017-12-12 09:43:17 -08001328 ['git', '-c', 'core.quotePath=false', 'ls-files', '--', '.'],
Josip Sokcevic0b123462021-06-08 20:41:32 +00001329 cwd=root).decode('utf-8', 'ignore').splitlines()
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001330
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001331
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001332def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001333 """Finds all presubmit files that apply to a given set of source files.
1334
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001335 If inherit-review-settings-ok is present right under root, looks for
1336 PRESUBMIT.py in directories enclosing root.
1337
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001338 Args:
1339 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001340 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001341
1342 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001343 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001344 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001345 files = [normpath(os.path.join(root, f)) for f in files]
1346
1347 # List all the individual directories containing files.
1348 directories = set([os.path.dirname(f) for f in files])
1349
1350 # Ignore root if inherit-review-settings-ok is present.
1351 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1352 root = None
1353
1354 # Collect all unique directories that may contain PRESUBMIT.py.
1355 candidates = set()
1356 for directory in directories:
1357 while True:
1358 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001359 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001360 candidates.add(directory)
1361 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001362 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001363 parent_dir = os.path.dirname(directory)
1364 if parent_dir == directory:
1365 # We hit the system root directory.
1366 break
1367 directory = parent_dir
1368
1369 # Look for PRESUBMIT.py in all candidate directories.
1370 results = []
1371 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -07001372 try:
1373 for f in os.listdir(directory):
1374 p = os.path.join(directory, f)
1375 if os.path.isfile(p) and re.match(
1376 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1377 results.append(p)
1378 except OSError:
1379 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001380
tobiasjs2836bcf2016-08-16 04:08:16 -07001381 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001382 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001383
1384
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001385class GetTryMastersExecuter(object):
1386 @staticmethod
1387 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1388 """Executes GetPreferredTryMasters() from a single presubmit script.
1389
1390 Args:
1391 script_text: The text of the presubmit script.
1392 presubmit_path: Project script to run.
1393 project: Project name to pass to presubmit script for bot selection.
1394
1395 Return:
1396 A map of try masters to map of builders to set of tests.
1397 """
1398 context = {}
1399 try:
Raul Tambre09e64b42019-05-14 01:57:22 +00001400 exec(compile(script_text, 'PRESUBMIT.py', 'exec', dont_inherit=True),
1401 context)
Raul Tambre7c938462019-05-24 16:35:35 +00001402 except Exception as e:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001403 raise PresubmitFailure('"%s" had an exception.\n%s'
1404 % (presubmit_path, e))
1405
1406 function_name = 'GetPreferredTryMasters'
1407 if function_name not in context:
1408 return {}
1409 get_preferred_try_masters = context[function_name]
1410 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1411 raise PresubmitFailure(
1412 'Expected function "GetPreferredTryMasters" to take two arguments.')
1413 return get_preferred_try_masters(project, change)
1414
1415
rmistry@google.com5626a922015-02-26 14:03:30 +00001416class GetPostUploadExecuter(object):
1417 @staticmethod
Edward Lemur016a0872020-02-04 22:13:28 +00001418 def ExecPresubmitScript(script_text, presubmit_path, gerrit_obj, change):
rmistry@google.com5626a922015-02-26 14:03:30 +00001419 """Executes PostUploadHook() from a single presubmit script.
1420
1421 Args:
1422 script_text: The text of the presubmit script.
1423 presubmit_path: Project script to run.
Edward Lemur016a0872020-02-04 22:13:28 +00001424 gerrit_obj: The GerritAccessor object.
rmistry@google.com5626a922015-02-26 14:03:30 +00001425 change: The Change object.
1426
1427 Return:
1428 A list of results objects.
1429 """
1430 context = {}
1431 try:
Raul Tambre09e64b42019-05-14 01:57:22 +00001432 exec(compile(script_text, 'PRESUBMIT.py', 'exec', dont_inherit=True),
1433 context)
Raul Tambre7c938462019-05-24 16:35:35 +00001434 except Exception as e:
rmistry@google.com5626a922015-02-26 14:03:30 +00001435 raise PresubmitFailure('"%s" had an exception.\n%s'
1436 % (presubmit_path, e))
1437
1438 function_name = 'PostUploadHook'
1439 if function_name not in context:
1440 return {}
1441 post_upload_hook = context[function_name]
1442 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1443 raise PresubmitFailure(
1444 'Expected function "PostUploadHook" to take three arguments.')
Edward Lemur016a0872020-02-04 22:13:28 +00001445 return post_upload_hook(gerrit_obj, change, OutputApi(False))
rmistry@google.com5626a922015-02-26 14:03:30 +00001446
1447
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001448def _MergeMasters(masters1, masters2):
1449 """Merges two master maps. Merges also the tests of each builder."""
1450 result = {}
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001451 for (master, builders) in itertools.chain(masters1.items(),
1452 masters2.items()):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001453 new_builders = result.setdefault(master, {})
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001454 for (builder, tests) in builders.items():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001455 new_builders.setdefault(builder, set([])).update(tests)
1456 return result
1457
1458
1459def DoGetTryMasters(change,
1460 changed_files,
1461 repository_root,
1462 default_presubmit,
1463 project,
1464 verbose,
1465 output_stream):
1466 """Get the list of try masters from the presubmit scripts.
1467
1468 Args:
1469 changed_files: List of modified files.
1470 repository_root: The repository root.
1471 default_presubmit: A default presubmit script to execute in any case.
1472 project: Optional name of a project used in selecting trybots.
1473 verbose: Prints debug info.
1474 output_stream: A stream to write debug output to.
1475
1476 Return:
1477 Map of try masters to map of builders to set of tests.
1478 """
1479 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1480 if not presubmit_files and verbose:
Edward Lemura5799e32020-01-17 19:26:51 +00001481 output_stream.write('Warning, no PRESUBMIT.py found.\n')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001482 results = {}
1483 executer = GetTryMastersExecuter()
1484
1485 if default_presubmit:
1486 if verbose:
Edward Lemura5799e32020-01-17 19:26:51 +00001487 output_stream.write('Running default presubmit script.\n')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001488 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1489 results = _MergeMasters(results, executer.ExecPresubmitScript(
1490 default_presubmit, fake_path, project, change))
1491 for filename in presubmit_files:
1492 filename = os.path.abspath(filename)
1493 if verbose:
Edward Lemura5799e32020-01-17 19:26:51 +00001494 output_stream.write('Running %s\n' % filename)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001495 # Accept CRLF presubmit script.
1496 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1497 results = _MergeMasters(results, executer.ExecPresubmitScript(
1498 presubmit_script, filename, project, change))
1499
1500 # Make sets to lists again for later JSON serialization.
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001501 for builders in results.values():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001502 for builder in builders:
1503 builders[builder] = list(builders[builder])
1504
1505 if results and verbose:
1506 output_stream.write('%s\n' % str(results))
1507 return results
1508
1509
rmistry@google.com5626a922015-02-26 14:03:30 +00001510def DoPostUploadExecuter(change,
Edward Lemur016a0872020-02-04 22:13:28 +00001511 gerrit_obj,
Edward Lemur6eb1d322020-02-27 22:20:15 +00001512 verbose):
rmistry@google.com5626a922015-02-26 14:03:30 +00001513 """Execute the post upload hook.
1514
1515 Args:
1516 change: The Change object.
Edward Lemur016a0872020-02-04 22:13:28 +00001517 gerrit_obj: The GerritAccessor object.
rmistry@google.com5626a922015-02-26 14:03:30 +00001518 verbose: Prints debug info.
rmistry@google.com5626a922015-02-26 14:03:30 +00001519 """
1520 presubmit_files = ListRelevantPresubmitFiles(
Edward Lemur6eb1d322020-02-27 22:20:15 +00001521 change.LocalPaths(), change.RepositoryRoot())
rmistry@google.com5626a922015-02-26 14:03:30 +00001522 if not presubmit_files and verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001523 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
rmistry@google.com5626a922015-02-26 14:03:30 +00001524 results = []
1525 executer = GetPostUploadExecuter()
1526 # The root presubmit file should be executed after the ones in subdirectories.
1527 # i.e. the specific post upload hooks should run before the general ones.
1528 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1529 presubmit_files.reverse()
1530
1531 for filename in presubmit_files:
1532 filename = os.path.abspath(filename)
1533 if verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001534 sys.stdout.write('Running %s\n' % filename)
rmistry@google.com5626a922015-02-26 14:03:30 +00001535 # Accept CRLF presubmit script.
1536 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1537 results.extend(executer.ExecPresubmitScript(
Edward Lemur016a0872020-02-04 22:13:28 +00001538 presubmit_script, filename, gerrit_obj, change))
rmistry@google.com5626a922015-02-26 14:03:30 +00001539
Edward Lemur6eb1d322020-02-27 22:20:15 +00001540 if not results:
1541 return 0
1542
1543 sys.stdout.write('\n')
1544 sys.stdout.write('** Post Upload Hook Messages **\n')
1545
1546 exit_code = 0
1547 for result in results:
1548 if result.fatal:
1549 exit_code = 1
1550 result.handle()
1551 sys.stdout.write('\n')
1552
1553 return exit_code
rmistry@google.com5626a922015-02-26 14:03:30 +00001554
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001555class PresubmitExecuter(object):
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001556 def __init__(self, change, committing, verbose, gerrit_obj, dry_run=None,
Dirk Pranke6f0df682021-06-25 00:42:33 +00001557 thread_pool=None, parallel=False, use_python3=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001558 """
1559 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001560 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001561 committing: True if 'git cl land' is running, False if 'git cl upload' is.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001562 gerrit_obj: provides basic Gerrit codereview functionality.
1563 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001564 parallel: if true, all tests reported via input_api.RunTests for all
1565 PRESUBMIT files will be run in parallel.
Dirk Pranke6f0df682021-06-25 00:42:33 +00001566 use_python3: if true, will use python3 instead of python2 by default
1567 if USE_PYTHON3 is not specified.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001568 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001569 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001570 self.committing = committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001571 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001572 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001573 self.dry_run = dry_run
Daniel Cheng7227d212017-11-17 08:12:37 -08001574 self.more_cc = []
Edward Lesmes8e282792018-04-03 18:50:29 -04001575 self.thread_pool = thread_pool
1576 self.parallel = parallel
Dirk Pranke6f0df682021-06-25 00:42:33 +00001577 self.use_python3 = use_python3
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001578
1579 def ExecPresubmitScript(self, script_text, presubmit_path):
1580 """Executes a single presubmit script.
1581
1582 Args:
1583 script_text: The text of the presubmit script.
1584 presubmit_path: The path to the presubmit file (this will be reported via
1585 input_api.PresubmitLocalPath()).
1586
1587 Return:
1588 A list of result objects, empty if no problems.
1589 """
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001590
chase@chromium.org8e416c82009-10-06 04:30:44 +00001591 # Change to the presubmit file's directory to support local imports.
1592 main_path = os.getcwd()
Saagar Sanghavi98b332f2020-07-31 17:19:15 +00001593 presubmit_dir = os.path.dirname(presubmit_path)
1594 os.chdir(presubmit_dir)
chase@chromium.org8e416c82009-10-06 04:30:44 +00001595
1596 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001597 input_api = InputApi(self.change, presubmit_path, self.committing,
Aaron Gable668c1d82018-04-03 10:19:16 -07001598 self.verbose, gerrit_obj=self.gerrit,
Edward Lesmes8e282792018-04-03 18:50:29 -04001599 dry_run=self.dry_run, thread_pool=self.thread_pool,
1600 parallel=self.parallel)
Daniel Cheng7227d212017-11-17 08:12:37 -08001601 output_api = OutputApi(self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001602 context = {}
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001603
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001604 # Try to figure out whether these presubmit checks should be run under
1605 # python2 or python3. We need to do this without actually trying to
1606 # compile the text, since the text might compile in one but not the
1607 # other.
Dirk Pranke6f0df682021-06-25 00:42:33 +00001608 m = re.search('^USE_PYTHON3 = (True|False)$', script_text,
1609 flags=re.MULTILINE)
1610 if m:
1611 use_python3 = m.group(1) == 'True'
1612 else:
1613 use_python3 = self.use_python3
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001614 if (((sys.version_info.major == 2) and use_python3) or
1615 ((sys.version_info.major == 3) and not use_python3)):
1616 return []
1617
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001618 try:
Raul Tambre09e64b42019-05-14 01:57:22 +00001619 exec(compile(script_text, 'PRESUBMIT.py', 'exec', dont_inherit=True),
1620 context)
Raul Tambre7c938462019-05-24 16:35:35 +00001621 except Exception as e:
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001622 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001623
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001624 context['__args'] = (input_api, output_api)
Ben Pastene8351dc12020-08-06 05:01:35 +00001625
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001626 # Get path of presubmit directory relative to repository root.
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001627 # Always use forward slashes, so that path is same in *nix and Windows
1628 root = input_api.change.RepositoryRoot()
1629 rel_path = os.path.relpath(presubmit_dir, root)
1630 rel_path = rel_path.replace(os.path.sep, '/')
Ben Pastene8351dc12020-08-06 05:01:35 +00001631
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001632 # Get the URL of git remote origin and use it to identify host and project
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001633 host = project = ''
1634 if self.gerrit:
1635 host = self.gerrit.host or ''
1636 project = self.gerrit.project or ''
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001637
1638 # Prefix for test names
1639 prefix = 'presubmit:%s/%s:%s/' % (host, project, rel_path)
1640
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001641 # Perform all the desired presubmit checks.
1642 results = []
Ben Pastene8351dc12020-08-06 05:01:35 +00001643
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001644 try:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001645 version = [
1646 int(x) for x in context.get('PRESUBMIT_VERSION', '0.0.0').split('.')
1647 ]
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001648
Scott Leecc2fe9b2020-11-19 19:38:06 +00001649 with rdb_wrapper.client(prefix) as sink:
1650 if version >= [2, 0, 0]:
1651 for function_name in context:
1652 if not function_name.startswith('Check'):
1653 continue
1654 if function_name.endswith('Commit') and not self.committing:
1655 continue
1656 if function_name.endswith('Upload') and self.committing:
1657 continue
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001658 logging.debug('Running %s in %s', function_name, presubmit_path)
1659 results.extend(
Scott Leecc2fe9b2020-11-19 19:38:06 +00001660 self._run_check_function(function_name, context, sink))
1661 logging.debug('Running %s done.', function_name)
1662 self.more_cc.extend(output_api.more_cc)
1663
1664 else: # Old format
1665 if self.committing:
1666 function_name = 'CheckChangeOnCommit'
1667 else:
1668 function_name = 'CheckChangeOnUpload'
1669 if function_name in context:
1670 logging.debug('Running %s in %s', function_name, presubmit_path)
1671 results.extend(
1672 self._run_check_function(function_name, context, sink))
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001673 logging.debug('Running %s done.', function_name)
1674 self.more_cc.extend(output_api.more_cc)
1675
1676 finally:
1677 for f in input_api._named_temporary_files:
1678 os.remove(f)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001679
chase@chromium.org8e416c82009-10-06 04:30:44 +00001680 # Return the process to the original working directory.
1681 os.chdir(main_path)
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001682 return results
1683
Scott Leecc2fe9b2020-11-19 19:38:06 +00001684 def _run_check_function(self, function_name, context, sink=None):
1685 """Evaluates and returns the result of a given presubmit function.
1686
1687 If sink is given, the result of the presubmit function will be reported
1688 to the ResultSink.
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001689
1690 Args:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001691 function_name: the name of the presubmit function to evaluate
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001692 context: a context dictionary in which the function will be evaluated
Scott Leecc2fe9b2020-11-19 19:38:06 +00001693 sink: an instance of ResultSink. None, by default.
1694 Returns:
1695 the result of the presubmit function call.
1696 """
1697 start_time = time_time()
1698 try:
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001699 result = eval(function_name + '(*__args)', context)
1700 self._check_result_type(result)
Allen Webbfe7d7092021-05-18 02:05:49 +00001701 except Exception:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001702 if sink:
1703 elapsed_time = time_time() - start_time
1704 sink.report(function_name, rdb_wrapper.STATUS_FAIL, elapsed_time)
Allen Webbfe7d7092021-05-18 02:05:49 +00001705 # TODO(crbug.com/953884): replace reraise with native py3:
Allen Webbb65bbfe2021-05-11 21:22:01 +00001706 # raise .. from e
Allen Webbfe7d7092021-05-18 02:05:49 +00001707 e_type, e_value, e_tb = sys.exc_info()
Dirk Pranke6fc394f2021-05-26 23:25:14 +00001708 print('Evaluation of %s failed: %s' % (function_name, e_value))
1709 six.reraise(e_type, e_value, e_tb)
Scott Leecc2fe9b2020-11-19 19:38:06 +00001710
Bruce Dawson2bdc49f2021-05-21 19:13:03 +00001711 elapsed_time = time_time() - start_time
1712 if elapsed_time > 10.0:
1713 sys.stdout.write(
1714 '%s took %.1fs to run.\n' % (function_name, elapsed_time))
Scott Leecc2fe9b2020-11-19 19:38:06 +00001715 if sink:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001716 status = rdb_wrapper.STATUS_PASS
1717 if any(r.fatal for r in result):
1718 status = rdb_wrapper.STATUS_FAIL
1719 sink.report(function_name, status, elapsed_time)
1720
1721 return result
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001722
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001723 def _check_result_type(self, result):
1724 """Helper function which ensures result is a list, and all elements are
1725 instances of OutputApi.PresubmitResult"""
1726 if not isinstance(result, (tuple, list)):
1727 raise PresubmitFailure('Presubmit functions must return a tuple or list')
1728 if not all(isinstance(res, OutputApi.PresubmitResult) for res in result):
1729 raise PresubmitFailure(
1730 'All presubmit results must be of types derived from '
1731 'output_api.PresubmitResult')
1732
1733
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001734def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001735 committing,
1736 verbose,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001737 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001738 may_prompt,
Aaron Gable668c1d82018-04-03 10:19:16 -07001739 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04001740 dry_run=None,
Debrian Figueroadd2737e2019-06-21 23:50:13 +00001741 parallel=False,
Dirk Pranke6f0df682021-06-25 00:42:33 +00001742 json_output=None,
1743 use_python3=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001744 """Runs all presubmit checks that apply to the files in the change.
1745
1746 This finds all PRESUBMIT.py files in directories enclosing the files in the
1747 change (up to the repository root) and calls the relevant entrypoint function
1748 depending on whether the change is being committed or uploaded.
1749
1750 Prints errors, warnings and notifications. Prompts the user for warnings
1751 when needed.
1752
1753 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001754 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001755 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001756 verbose: Prints debug info.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001757 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001758 may_prompt: Enable (y/n) questions on warning or error. If False,
1759 any questions are answered with yes by default.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001760 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001761 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001762 parallel: if true, all tests specified by input_api.RunTests in all
1763 PRESUBMIT files will be run in parallel.
Dirk Pranke6f0df682021-06-25 00:42:33 +00001764 use_python3: if true, default to using Python3 for presubmit checks
1765 rather than Python2.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001766 Return:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001767 1 if presubmit checks failed or 0 otherwise.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001768 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001769 old_environ = os.environ
1770 try:
1771 # Make sure python subprocesses won't generate .pyc files.
1772 os.environ = os.environ.copy()
Edward Lemurb9830242019-10-30 22:19:20 +00001773 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001774
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001775 python_version = 'Python %s' % sys.version_info.major
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001776 if committing:
Dirk Pranke6fc394f2021-05-26 23:25:14 +00001777 sys.stdout.write('Running %s presubmit commit checks ...\n' %
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001778 python_version)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001779 else:
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001780 sys.stdout.write('Running %s presubmit upload checks ...\n' %
1781 python_version)
Edward Lemurecc27072020-01-06 16:42:34 +00001782 start_time = time_time()
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001783 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001784 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001785 if not presubmit_files and verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001786 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001787 results = []
Edward Lesmes8e282792018-04-03 18:50:29 -04001788 thread_pool = ThreadPool()
Edward Lemur7e3c67f2018-07-20 20:52:49 +00001789 executer = PresubmitExecuter(change, committing, verbose, gerrit_obj,
Dirk Pranke6f0df682021-06-25 00:42:33 +00001790 dry_run, thread_pool, parallel, use_python3)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001791 if default_presubmit:
1792 if verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001793 sys.stdout.write('Running default presubmit script.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001794 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1795 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1796 for filename in presubmit_files:
1797 filename = os.path.abspath(filename)
1798 if verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001799 sys.stdout.write('Running %s\n' % filename)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001800 # Accept CRLF presubmit script.
1801 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1802 results += executer.ExecPresubmitScript(presubmit_script, filename)
Edward Lesmes8e282792018-04-03 18:50:29 -04001803 results += thread_pool.RunAsync()
1804
Edward Lemur6eb1d322020-02-27 22:20:15 +00001805 messages = {}
1806 should_prompt = False
1807 presubmits_failed = False
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001808 for result in results:
1809 if result.fatal:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001810 presubmits_failed = True
1811 messages.setdefault('ERRORS', []).append(result)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001812 elif result.should_prompt:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001813 should_prompt = True
1814 messages.setdefault('Warnings', []).append(result)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001815 else:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001816 messages.setdefault('Messages', []).append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001817
Edward Lemur6eb1d322020-02-27 22:20:15 +00001818 sys.stdout.write('\n')
1819 for name, items in messages.items():
1820 sys.stdout.write('** Presubmit %s **\n' % name)
1821 for item in items:
1822 item.handle()
1823 sys.stdout.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001824
Edward Lemurecc27072020-01-06 16:42:34 +00001825 total_time = time_time() - start_time
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001826 if total_time > 1.0:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001827 sys.stdout.write(
1828 'Presubmit checks took %.1fs to calculate.\n\n' % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001829
Edward Lemur6eb1d322020-02-27 22:20:15 +00001830 if not should_prompt and not presubmits_failed:
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001831 sys.stdout.write('%s presubmit checks passed.\n' % python_version)
Edward Lemur6eb1d322020-02-27 22:20:15 +00001832 elif should_prompt:
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001833 sys.stdout.write('There were %s presubmit warnings. ' % python_version)
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001834 if may_prompt:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001835 presubmits_failed = not prompt_should_continue(
1836 'Are you sure you wish to continue? (y/N): ')
Garrett Beatyacb807e2020-08-28 18:17:24 +00001837 else:
1838 sys.stdout.write('\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001839
Edward Lemur1dc66e12020-02-21 21:36:34 +00001840 if json_output:
1841 # Write the presubmit results to json output
1842 presubmit_results = {
1843 'errors': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001844 error.json_format()
1845 for error in messages.get('ERRORS', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001846 ],
1847 'notifications': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001848 notification.json_format()
1849 for notification in messages.get('Messages', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001850 ],
1851 'warnings': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001852 warning.json_format()
1853 for warning in messages.get('Warnings', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001854 ],
Edward Lemur6eb1d322020-02-27 22:20:15 +00001855 'more_cc': executer.more_cc,
Edward Lemur1dc66e12020-02-21 21:36:34 +00001856 }
1857
1858 gclient_utils.FileWrite(
1859 json_output, json.dumps(presubmit_results, sort_keys=True))
1860
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001861 global _ASKED_FOR_FEEDBACK
1862 # Ask for feedback one time out of 5.
1863 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
Edward Lemur6eb1d322020-02-27 22:20:15 +00001864 sys.stdout.write(
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001865 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1866 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1867 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001868 _ASKED_FOR_FEEDBACK = True
Edward Lemur6eb1d322020-02-27 22:20:15 +00001869
1870 return 1 if presubmits_failed else 0
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001871 finally:
1872 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001873
1874
Edward Lemur50984a62020-02-06 18:10:18 +00001875def _scan_sub_dirs(mask, recursive):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001876 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001877 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001878
1879 results = []
1880 for root, dirs, files in os.walk('.'):
1881 if '.svn' in dirs:
1882 dirs.remove('.svn')
1883 if '.git' in dirs:
1884 dirs.remove('.git')
1885 for name in files:
1886 if fnmatch.fnmatch(name, mask):
1887 results.append(os.path.join(root, name))
1888 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001889
1890
Edward Lemur50984a62020-02-06 18:10:18 +00001891def _parse_files(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001892 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001893 files = []
1894 for arg in args:
Edward Lemur50984a62020-02-06 18:10:18 +00001895 files.extend([('M', f) for f in _scan_sub_dirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001896 return files
1897
1898
Edward Lemur50984a62020-02-06 18:10:18 +00001899def _parse_change(parser, options):
1900 """Process change options.
1901
1902 Args:
1903 parser: The parser used to parse the arguments from command line.
1904 options: The arguments parsed from command line.
1905 Returns:
1906 A GitChange if the change root is a git repository, or a Change otherwise.
1907 """
1908 if options.files and options.all_files:
1909 parser.error('<files> cannot be specified when --all-files is set.')
1910
Edward Lesmes44ea3ff2020-02-05 00:14:30 +00001911 change_scm = scm.determine_scm(options.root)
Edward Lemur50984a62020-02-06 18:10:18 +00001912 if change_scm != 'git' and not options.files:
1913 parser.error('<files> is not optional for unversioned directories.')
1914
1915 if options.files:
1916 change_files = _parse_files(options.files, options.recursive)
1917 elif options.all_files:
1918 change_files = [('M', f) for f in scm.GIT.GetAllFiles(options.root)]
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001919 else:
Edward Lemur50984a62020-02-06 18:10:18 +00001920 change_files = scm.GIT.CaptureStatus(
Edward Lemur7f6dec02020-02-06 20:23:58 +00001921 options.root, options.upstream or None)
Edward Lemur50984a62020-02-06 18:10:18 +00001922
1923 logging.info('Found %d file(s).', len(change_files))
1924
1925 change_class = GitChange if change_scm == 'git' else Change
1926 return change_class(
1927 options.name,
1928 options.description,
1929 options.root,
1930 change_files,
1931 options.issue,
1932 options.patchset,
1933 options.author,
1934 upstream=options.upstream)
1935
1936
1937def _parse_gerrit_options(parser, options):
1938 """Process gerrit options.
1939
1940 SIDE EFFECTS: Modifies options.author and options.description from Gerrit if
1941 options.gerrit_fetch is set.
1942
1943 Args:
1944 parser: The parser used to parse the arguments from command line.
1945 options: The arguments parsed from command line.
1946 Returns:
Stephen Martinisfb09de22021-02-25 03:41:13 +00001947 A GerritAccessor object if options.gerrit_url is set, or None otherwise.
Edward Lemur50984a62020-02-06 18:10:18 +00001948 """
1949 gerrit_obj = None
Stephen Martinisfb09de22021-02-25 03:41:13 +00001950 if options.gerrit_url:
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001951 gerrit_obj = GerritAccessor(
1952 url=options.gerrit_url,
1953 project=options.gerrit_project,
1954 branch=options.gerrit_branch)
Edward Lemur50984a62020-02-06 18:10:18 +00001955
1956 if not options.gerrit_fetch:
1957 return gerrit_obj
1958
1959 if not options.gerrit_url or not options.issue or not options.patchset:
1960 parser.error(
1961 '--gerrit_fetch requires --gerrit_url, --issue and --patchset.')
1962
1963 options.author = gerrit_obj.GetChangeOwner(options.issue)
1964 options.description = gerrit_obj.GetChangeDescription(
1965 options.issue, options.patchset)
1966
1967 logging.info('Got author: "%s"', options.author)
1968 logging.info('Got description: """\n%s\n"""', options.description)
1969
1970 return gerrit_obj
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001971
1972
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001973@contextlib.contextmanager
1974def canned_check_filter(method_names):
1975 filtered = {}
1976 try:
1977 for method_name in method_names:
1978 if not hasattr(presubmit_canned_checks, method_name):
Gavin Make6a62332020-12-04 21:57:10 +00001979 logging.warning('Skipping unknown "canned" check %s' % method_name)
Aaron Gableecee74c2018-04-02 15:13:08 -07001980 continue
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001981 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1982 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1983 yield
1984 finally:
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001985 for name, method in filtered.items():
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001986 setattr(presubmit_canned_checks, name, method)
1987
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001988
sbc@chromium.org013731e2015-02-26 18:28:43 +00001989def main(argv=None):
Edward Lemura5799e32020-01-17 19:26:51 +00001990 parser = argparse.ArgumentParser(usage='%(prog)s [options] <files...>')
1991 hooks = parser.add_mutually_exclusive_group()
1992 hooks.add_argument('-c', '--commit', action='store_true',
1993 help='Use commit instead of upload checks.')
1994 hooks.add_argument('-u', '--upload', action='store_false', dest='commit',
1995 help='Use upload instead of commit checks.')
Edward Lemur75526302020-02-27 22:31:05 +00001996 hooks.add_argument('--post_upload', action='store_true',
1997 help='Run post-upload commit hooks.')
Edward Lemura5799e32020-01-17 19:26:51 +00001998 parser.add_argument('-r', '--recursive', action='store_true',
1999 help='Act recursively.')
2000 parser.add_argument('-v', '--verbose', action='count', default=0,
2001 help='Use 2 times for more debug info.')
2002 parser.add_argument('--name', default='no name')
2003 parser.add_argument('--author')
Edward Lemur227d5102020-02-25 23:45:35 +00002004 desc = parser.add_mutually_exclusive_group()
2005 desc.add_argument('--description', default='', help='The change description.')
2006 desc.add_argument('--description_file',
2007 help='File to read change description from.')
Edward Lemura5799e32020-01-17 19:26:51 +00002008 parser.add_argument('--issue', type=int, default=0)
2009 parser.add_argument('--patchset', type=int, default=0)
2010 parser.add_argument('--root', default=os.getcwd(),
2011 help='Search for PRESUBMIT.py up to this directory. '
2012 'If inherit-review-settings-ok is present in this '
2013 'directory, parent directories up to the root file '
2014 'system directories will also be searched.')
2015 parser.add_argument('--upstream',
2016 help='Git only: the base ref or upstream branch against '
2017 'which the diff should be computed.')
2018 parser.add_argument('--default_presubmit')
2019 parser.add_argument('--may_prompt', action='store_true', default=False)
2020 parser.add_argument('--skip_canned', action='append', default=[],
2021 help='A list of checks to skip which appear in '
2022 'presubmit_canned_checks. Can be provided multiple times '
2023 'to skip multiple canned checks.')
2024 parser.add_argument('--dry_run', action='store_true', help=argparse.SUPPRESS)
2025 parser.add_argument('--gerrit_url', help=argparse.SUPPRESS)
Edward Lesmeseb1bd622021-03-01 19:54:07 +00002026 parser.add_argument('--gerrit_project', help=argparse.SUPPRESS)
2027 parser.add_argument('--gerrit_branch', help=argparse.SUPPRESS)
Edward Lemura5799e32020-01-17 19:26:51 +00002028 parser.add_argument('--gerrit_fetch', action='store_true',
2029 help=argparse.SUPPRESS)
2030 parser.add_argument('--parallel', action='store_true',
2031 help='Run all tests specified by input_api.RunTests in '
2032 'all PRESUBMIT files in parallel.')
2033 parser.add_argument('--json_output',
2034 help='Write presubmit errors to json output.')
Edward Lemur1dc66e12020-02-21 21:36:34 +00002035 parser.add_argument('--all_files', action='store_true',
Edward Lemur50984a62020-02-06 18:10:18 +00002036 help='Mark all files under source control as modified.')
Edward Lemura5799e32020-01-17 19:26:51 +00002037 parser.add_argument('files', nargs='*',
2038 help='List of files to be marked as modified when '
2039 'executing presubmit or post-upload hooks. fnmatch '
2040 'wildcards can also be used.')
Dirk Pranke6f0df682021-06-25 00:42:33 +00002041 parser.add_argument('--use-python3', action='store_true',
2042 help='Use python3 for presubmit checks by default')
Edward Lemura5799e32020-01-17 19:26:51 +00002043 options = parser.parse_args(argv)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00002044
Erik Staabcca5c492020-04-16 17:40:07 +00002045 log_level = logging.ERROR
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002046 if options.verbose >= 2:
Erik Staabcca5c492020-04-16 17:40:07 +00002047 log_level = logging.DEBUG
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002048 elif options.verbose:
Erik Staabcca5c492020-04-16 17:40:07 +00002049 log_level = logging.INFO
2050 log_format = ('[%(levelname).1s%(asctime)s %(process)d %(thread)d '
2051 '%(filename)s] %(message)s')
2052 logging.basicConfig(format=log_format, level=log_level)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00002053
Edward Lemur227d5102020-02-25 23:45:35 +00002054 if options.description_file:
2055 options.description = gclient_utils.FileRead(options.description_file)
Edward Lemur50984a62020-02-06 18:10:18 +00002056 gerrit_obj = _parse_gerrit_options(parser, options)
2057 change = _parse_change(parser, options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00002058
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002059 try:
Edward Lemur75526302020-02-27 22:31:05 +00002060 if options.post_upload:
2061 return DoPostUploadExecuter(
2062 change,
2063 gerrit_obj,
2064 options.verbose)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002065 with canned_check_filter(options.skip_canned):
Edward Lemur6eb1d322020-02-27 22:20:15 +00002066 return DoPresubmitChecks(
Edward Lemur50984a62020-02-06 18:10:18 +00002067 change,
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002068 options.commit,
2069 options.verbose,
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00002070 options.default_presubmit,
2071 options.may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002072 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04002073 options.dry_run,
Debrian Figueroadd2737e2019-06-21 23:50:13 +00002074 options.parallel,
Dirk Pranke6f0df682021-06-25 00:42:33 +00002075 options.json_output,
2076 options.use_python3)
Raul Tambre7c938462019-05-24 16:35:35 +00002077 except PresubmitFailure as e:
Raul Tambre80ee78e2019-05-06 22:41:05 +00002078 print(e, file=sys.stderr)
2079 print('Maybe your depot_tools is out of date?', file=sys.stderr)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002080 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002081
2082
2083if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00002084 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00002085 try:
2086 sys.exit(main())
2087 except KeyboardInterrupt:
2088 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07002089 sys.exit(2)