blob: 3a05c4b8efcd531680e774b9f009eed34ff9062b [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
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import sys # Parts exposed through API.
33import tempfile # Exposed through the API.
Edward Lesmes8e282792018-04-03 18:50:29 -040034import threading
jam@chromium.org2a891dc2009-08-20 20:33:37 +000035import time
Edward Lemurde9e3ca2019-10-24 21:13:31 +000036import traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +000037import unittest # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000038from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000039
40# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000041import fix_encoding
Yoshisato Yanagisawa04600b42019-03-15 03:03:41 +000042import gclient_paths # Exposed through the API
43import gclient_utils
Aaron Gableb584c4f2017-04-26 16:28:08 -070044import git_footers
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000045import gerrit_util
Edward Lesmes9ce03f82021-01-12 20:13:31 +000046import owners as owners_db
47import owners_client
Jochen Eisinger76f5fc62017-04-07 16:27:46 +020048import owners_finder
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000049import presubmit_canned_checks
Saagar Sanghavi9949ab72020-07-20 20:56:40 +000050import rdb_wrapper
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000051import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000052import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053
Edward Lemur16af3562019-10-17 22:11:33 +000054if sys.version_info.major == 2:
55 # TODO(1009814): Expose urllib2 only through urllib_request and urllib_error
56 import urllib2 # Exposed through the API.
57 import urlparse
58 import urllib2 as urllib_request
59 import urllib2 as urllib_error
60else:
61 import urllib.parse as urlparse
62 import urllib.request as urllib_request
63 import urllib.error as urllib_error
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000064
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000065# Ask for feedback only once in program lifetime.
66_ASKED_FOR_FEEDBACK = False
67
Edward Lemurecc27072020-01-06 16:42:34 +000068def time_time():
69 # Use this so that it can be mocked in tests without interfering with python
70 # system machinery.
71 return time.time()
72
73
maruel@chromium.org899e1c12011-04-07 17:03:18 +000074class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000075 pass
76
77
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000078class CommandData(object):
Edward Lemur940c2822019-08-23 00:34:25 +000079 def __init__(self, name, cmd, kwargs, message, python3=False):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000080 self.name = name
81 self.cmd = cmd
Edward Lesmes8e282792018-04-03 18:50:29 -040082 self.stdin = kwargs.get('stdin', None)
Edward Lemur2d6b67c2019-08-23 22:25:41 +000083 self.kwargs = kwargs.copy()
Edward Lesmes8e282792018-04-03 18:50:29 -040084 self.kwargs['stdout'] = subprocess.PIPE
85 self.kwargs['stderr'] = subprocess.STDOUT
86 self.kwargs['stdin'] = subprocess.PIPE
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000087 self.message = message
88 self.info = None
Edward Lemur940c2822019-08-23 00:34:25 +000089 self.python3 = python3
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000090
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000091
Edward Lesmes8e282792018-04-03 18:50:29 -040092# Adapted from
93# https://github.com/google/gtest-parallel/blob/master/gtest_parallel.py#L37
94#
95# An object that catches SIGINT sent to the Python process and notices
96# if processes passed to wait() die by SIGINT (we need to look for
97# both of those cases, because pressing Ctrl+C can result in either
98# the main process or one of the subprocesses getting the signal).
99#
100# Before a SIGINT is seen, wait(p) will simply call p.wait() and
101# return the result. Once a SIGINT has been seen (in the main process
102# or a subprocess, including the one the current call is waiting for),
Edward Lemur9a5bb612019-09-26 02:01:52 +0000103# wait(p) will call p.terminate().
Edward Lesmes8e282792018-04-03 18:50:29 -0400104class SigintHandler(object):
Edward Lesmes8e282792018-04-03 18:50:29 -0400105 sigint_returncodes = {-signal.SIGINT, # Unix
106 -1073741510, # Windows
107 }
108 def __init__(self):
109 self.__lock = threading.Lock()
110 self.__processes = set()
111 self.__got_sigint = False
Edward Lemur9a5bb612019-09-26 02:01:52 +0000112 self.__previous_signal = signal.signal(signal.SIGINT, self.interrupt)
Edward Lesmes8e282792018-04-03 18:50:29 -0400113
114 def __on_sigint(self):
115 self.__got_sigint = True
116 while self.__processes:
117 try:
118 self.__processes.pop().terminate()
119 except OSError:
120 pass
121
Edward Lemur9a5bb612019-09-26 02:01:52 +0000122 def interrupt(self, signal_num, frame):
Edward Lesmes8e282792018-04-03 18:50:29 -0400123 with self.__lock:
124 self.__on_sigint()
Edward Lemur9a5bb612019-09-26 02:01:52 +0000125 self.__previous_signal(signal_num, frame)
Edward Lesmes8e282792018-04-03 18:50:29 -0400126
127 def got_sigint(self):
128 with self.__lock:
129 return self.__got_sigint
130
131 def wait(self, p, stdin):
132 with self.__lock:
133 if self.__got_sigint:
134 p.terminate()
135 self.__processes.add(p)
136 stdout, stderr = p.communicate(stdin)
137 code = p.returncode
138 with self.__lock:
139 self.__processes.discard(p)
140 if code in self.sigint_returncodes:
141 self.__on_sigint()
Edward Lesmes8e282792018-04-03 18:50:29 -0400142 return stdout, stderr
143
144sigint_handler = SigintHandler()
145
146
Edward Lemurecc27072020-01-06 16:42:34 +0000147class Timer(object):
148 def __init__(self, timeout, fn):
149 self.completed = False
150 self._fn = fn
151 self._timer = threading.Timer(timeout, self._onTimer) if timeout else None
152
153 def __enter__(self):
154 if self._timer:
155 self._timer.start()
156 return self
157
158 def __exit__(self, _type, _value, _traceback):
159 if self._timer:
160 self._timer.cancel()
161
162 def _onTimer(self):
163 self._fn()
164 self.completed = True
165
166
Edward Lesmes8e282792018-04-03 18:50:29 -0400167class ThreadPool(object):
Edward Lemurecc27072020-01-06 16:42:34 +0000168 def __init__(self, pool_size=None, timeout=None):
169 self.timeout = timeout
Edward Lesmes8e282792018-04-03 18:50:29 -0400170 self._pool_size = pool_size or multiprocessing.cpu_count()
171 self._messages = []
172 self._messages_lock = threading.Lock()
173 self._tests = []
174 self._tests_lock = threading.Lock()
175 self._nonparallel_tests = []
176
Edward Lemurecc27072020-01-06 16:42:34 +0000177 def _GetCommand(self, test):
Edward Lemur940c2822019-08-23 00:34:25 +0000178 vpython = 'vpython'
179 if test.python3:
180 vpython += '3'
181 if sys.platform == 'win32':
182 vpython += '.bat'
Edward Lesmes8e282792018-04-03 18:50:29 -0400183
184 cmd = test.cmd
185 if cmd[0] == 'python':
186 cmd = list(cmd)
187 cmd[0] = vpython
188 elif cmd[0].endswith('.py'):
189 cmd = [vpython] + cmd
190
Edward Lemur336e51f2019-11-14 21:42:04 +0000191 # On Windows, scripts on the current directory take precedence over PATH, so
192 # that when testing depot_tools on Windows, calling `vpython.bat` will
193 # execute the copy of vpython of the depot_tools under test instead of the
194 # one in the bot.
195 # As a workaround, we run the tests from the parent directory instead.
196 if (cmd[0] == vpython and
197 'cwd' in test.kwargs and
198 os.path.basename(test.kwargs['cwd']) == 'depot_tools'):
199 test.kwargs['cwd'] = os.path.dirname(test.kwargs['cwd'])
200 cmd[1] = os.path.join('depot_tools', cmd[1])
201
Edward Lemurecc27072020-01-06 16:42:34 +0000202 return cmd
203
204 def _RunWithTimeout(self, cmd, stdin, kwargs):
205 p = subprocess.Popen(cmd, **kwargs)
206 with Timer(self.timeout, p.terminate) as timer:
207 stdout, _ = sigint_handler.wait(p, stdin)
208 if timer.completed:
209 stdout = 'Process timed out after %ss\n%s' % (self.timeout, stdout)
210 return p.returncode, stdout
211
212 def CallCommand(self, test):
213 """Runs an external program.
214
Edward Lemura5799e32020-01-17 19:26:51 +0000215 This function converts invocation of .py files and invocations of 'python'
Edward Lemurecc27072020-01-06 16:42:34 +0000216 to vpython invocations.
217 """
218 cmd = self._GetCommand(test)
Edward Lesmes8e282792018-04-03 18:50:29 -0400219 try:
Edward Lemurecc27072020-01-06 16:42:34 +0000220 start = time_time()
221 returncode, stdout = self._RunWithTimeout(cmd, test.stdin, test.kwargs)
222 duration = time_time() - start
Edward Lemur7cf94382019-11-15 22:36:41 +0000223 except Exception:
Edward Lemurecc27072020-01-06 16:42:34 +0000224 duration = time_time() - start
Edward Lesmes8e282792018-04-03 18:50:29 -0400225 return test.message(
Edward Lemur7cf94382019-11-15 22:36:41 +0000226 '%s\n%s exec failure (%4.2fs)\n%s' % (
227 test.name, ' '.join(cmd), duration, traceback.format_exc()))
Edward Lemurde9e3ca2019-10-24 21:13:31 +0000228
Edward Lemurecc27072020-01-06 16:42:34 +0000229 if returncode != 0:
Edward Lesmes8e282792018-04-03 18:50:29 -0400230 return test.message(
Edward Lemur7cf94382019-11-15 22:36:41 +0000231 '%s\n%s (%4.2fs) failed\n%s' % (
232 test.name, ' '.join(cmd), duration, stdout))
Edward Lemurecc27072020-01-06 16:42:34 +0000233
Edward Lesmes8e282792018-04-03 18:50:29 -0400234 if test.info:
Edward Lemur7cf94382019-11-15 22:36:41 +0000235 return test.info('%s\n%s (%4.2fs)' % (test.name, ' '.join(cmd), duration))
Edward Lesmes8e282792018-04-03 18:50:29 -0400236
237 def AddTests(self, tests, parallel=True):
238 if parallel:
239 self._tests.extend(tests)
240 else:
241 self._nonparallel_tests.extend(tests)
242
243 def RunAsync(self):
244 self._messages = []
245
246 def _WorkerFn():
247 while True:
248 test = None
249 with self._tests_lock:
250 if not self._tests:
251 break
252 test = self._tests.pop()
253 result = self.CallCommand(test)
254 if result:
255 with self._messages_lock:
256 self._messages.append(result)
257
258 def _StartDaemon():
259 t = threading.Thread(target=_WorkerFn)
260 t.daemon = True
261 t.start()
262 return t
263
264 while self._nonparallel_tests:
265 test = self._nonparallel_tests.pop()
266 result = self.CallCommand(test)
267 if result:
268 self._messages.append(result)
269
270 if self._tests:
271 threads = [_StartDaemon() for _ in range(self._pool_size)]
272 for worker in threads:
273 worker.join()
274
275 return self._messages
276
277
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000278def normpath(path):
279 '''Version of os.path.normpath that also changes backward slashes to
280 forward slashes when not running on Windows.
281 '''
282 # This is safe to always do because the Windows version of os.path.normpath
283 # will replace forward slashes with backward slashes.
284 path = path.replace(os.sep, '/')
285 return os.path.normpath(path)
286
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000287
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000288def _RightHandSideLinesImpl(affected_files):
289 """Implements RightHandSideLines for InputApi and GclChange."""
290 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000291 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000292 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000293 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000294
295
Edward Lemur6eb1d322020-02-27 22:20:15 +0000296def prompt_should_continue(prompt_string):
297 sys.stdout.write(prompt_string)
298 response = sys.stdin.readline().strip().lower()
299 return response in ('y', 'yes')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000300
301
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000302# Top level object so multiprocessing can pickle
303# Public access through OutputApi object.
304class _PresubmitResult(object):
305 """Base class for result objects."""
306 fatal = False
307 should_prompt = False
308
309 def __init__(self, message, items=None, long_text=''):
310 """
311 message: A short one-line message to indicate errors.
312 items: A list of short strings to indicate where errors occurred.
313 long_text: multi-line text output, e.g. from another tool
314 """
315 self._message = message
316 self._items = items or []
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000317 self._long_text = long_text.rstrip()
318
Edward Lemur6eb1d322020-02-27 22:20:15 +0000319 def handle(self):
320 sys.stdout.write(self._message)
321 sys.stdout.write('\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000322 for index, item in enumerate(self._items):
Edward Lemur6eb1d322020-02-27 22:20:15 +0000323 sys.stdout.write(' ')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000324 # Write separately in case it's unicode.
Edward Lemur6eb1d322020-02-27 22:20:15 +0000325 sys.stdout.write(str(item))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000326 if index < len(self._items) - 1:
Edward Lemur6eb1d322020-02-27 22:20:15 +0000327 sys.stdout.write(' \\')
328 sys.stdout.write('\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000329 if self._long_text:
Edward Lemur6eb1d322020-02-27 22:20:15 +0000330 sys.stdout.write('\n***************\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000331 # Write separately in case it's unicode.
Edward Lemur6eb1d322020-02-27 22:20:15 +0000332 sys.stdout.write(self._long_text)
333 sys.stdout.write('\n***************\n')
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000334
Debrian Figueroadd2737e2019-06-21 23:50:13 +0000335 def json_format(self):
336 return {
337 'message': self._message,
Debrian Figueroa6095d402019-06-28 18:47:18 +0000338 'items': [str(item) for item in self._items],
Debrian Figueroadd2737e2019-06-21 23:50:13 +0000339 'long_text': self._long_text,
340 'fatal': self.fatal
341 }
342
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000343
344# Top level object so multiprocessing can pickle
345# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000346class _PresubmitError(_PresubmitResult):
347 """A hard presubmit error."""
348 fatal = True
349
350
351# Top level object so multiprocessing can pickle
352# Public access through OutputApi object.
353class _PresubmitPromptWarning(_PresubmitResult):
354 """An warning that prompts the user if they want to continue."""
355 should_prompt = True
356
357
358# Top level object so multiprocessing can pickle
359# Public access through OutputApi object.
360class _PresubmitNotifyResult(_PresubmitResult):
361 """Just print something to the screen -- but it's not even a warning."""
362 pass
363
364
365# Top level object so multiprocessing can pickle
366# Public access through OutputApi object.
367class _MailTextResult(_PresubmitResult):
368 """A warning that should be included in the review request email."""
369 def __init__(self, *args, **kwargs):
370 super(_MailTextResult, self).__init__()
371 raise NotImplementedError()
372
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000373class GerritAccessor(object):
374 """Limited Gerrit functionality for canned presubmit checks to work.
375
376 To avoid excessive Gerrit calls, caches the results.
377 """
378
Edward Lesmeseb1bd622021-03-01 19:54:07 +0000379 def __init__(self, url=None, project=None, branch=None):
380 self.host = urlparse.urlparse(url).netloc if url else None
381 self.project = project
382 self.branch = branch
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000383 self.cache = {}
384
385 def _FetchChangeDetail(self, issue):
386 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100387 try:
388 return gerrit_util.GetChangeDetail(
389 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700390 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100391 except gerrit_util.GerritError as e:
392 if e.http_status == 404:
393 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
394 'no credentials to fetch issue details' % issue)
395 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000396
397 def GetChangeInfo(self, issue):
398 """Returns labels and all revisions (patchsets) for this issue.
399
400 The result is a dictionary according to Gerrit REST Api.
401 https://gerrit-review.googlesource.com/Documentation/rest-api.html
402
403 However, API isn't very clear what's inside, so see tests for example.
404 """
405 assert issue
406 cache_key = int(issue)
407 if cache_key not in self.cache:
408 self.cache[cache_key] = self._FetchChangeDetail(issue)
409 return self.cache[cache_key]
410
411 def GetChangeDescription(self, issue, patchset=None):
412 """If patchset is none, fetches current patchset."""
413 info = self.GetChangeInfo(issue)
414 # info is a reference to cache. We'll modify it here adding description to
415 # it to the right patchset, if it is not yet there.
416
417 # Find revision info for the patchset we want.
418 if patchset is not None:
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000419 for rev, rev_info in info['revisions'].items():
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000420 if str(rev_info['_number']) == str(patchset):
421 break
422 else:
423 raise Exception('patchset %s doesn\'t exist in issue %s' % (
424 patchset, issue))
425 else:
426 rev = info['current_revision']
427 rev_info = info['revisions'][rev]
428
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100429 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000430
Mun Yong Jang603d01e2017-12-19 16:38:30 -0800431 def GetDestRef(self, issue):
432 ref = self.GetChangeInfo(issue)['branch']
433 if not ref.startswith('refs/'):
434 # NOTE: it is possible to create 'refs/x' branch,
435 # aka 'refs/heads/refs/x'. However, this is ill-advised.
436 ref = 'refs/heads/%s' % ref
437 return ref
438
Edward Lesmes02d4b822020-11-11 00:37:35 +0000439 def _GetApproversForLabel(self, issue, label):
440 change_info = self.GetChangeInfo(issue)
441 label_info = change_info.get('labels', {}).get(label, {})
442 values = label_info.get('values', {}).keys()
443 if not values:
444 return []
445 max_value = max(int(v) for v in values)
446 return [v for v in label_info.get('all', [])
447 if v.get('value', 0) == max_value]
448
Edward Lesmesc4566172021-03-19 16:55:13 +0000449 def IsBotCommitApproved(self, issue):
450 return bool(self._GetApproversForLabel(issue, 'Bot-Commit'))
451
Edward Lesmescf49cb82020-11-11 01:08:36 +0000452 def IsOwnersOverrideApproved(self, issue):
453 return bool(self._GetApproversForLabel(issue, 'Owners-Override'))
454
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000455 def GetChangeOwner(self, issue):
456 return self.GetChangeInfo(issue)['owner']['email']
457
458 def GetChangeReviewers(self, issue, approving_only=True):
Aaron Gable8b478f02017-07-31 15:33:19 -0700459 changeinfo = self.GetChangeInfo(issue)
460 if approving_only:
Edward Lesmes02d4b822020-11-11 00:37:35 +0000461 reviewers = self._GetApproversForLabel(issue, 'Code-Review')
Aaron Gable8b478f02017-07-31 15:33:19 -0700462 else:
463 reviewers = changeinfo.get('reviewers', {}).get('REVIEWER', [])
464 return [r.get('email') for r in reviewers]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000465
Edward Lemure4d329c2020-02-03 20:41:18 +0000466 def UpdateDescription(self, description, issue):
467 gerrit_util.SetCommitMessage(self.host, issue, description, notify='NONE')
468
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000469
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000470class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000471 """An instance of OutputApi gets passed to presubmit scripts so that they
472 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000473 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000474 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000475 PresubmitError = _PresubmitError
476 PresubmitPromptWarning = _PresubmitPromptWarning
477 PresubmitNotifyResult = _PresubmitNotifyResult
478 MailTextResult = _MailTextResult
479
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000480 def __init__(self, is_committing):
481 self.is_committing = is_committing
Daniel Cheng7227d212017-11-17 08:12:37 -0800482 self.more_cc = []
483
484 def AppendCC(self, cc):
485 """Appends a user to cc for this change."""
486 self.more_cc.append(cc)
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000487
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000488 def PresubmitPromptOrNotify(self, *args, **kwargs):
489 """Warn the user when uploading, but only notify if committing."""
490 if self.is_committing:
491 return self.PresubmitNotifyResult(*args, **kwargs)
492 return self.PresubmitPromptWarning(*args, **kwargs)
493
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000494
495class InputApi(object):
496 """An instance of this object is passed to presubmit scripts so they can
497 know stuff about the change they're looking at.
498 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000499 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800500 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000501
maruel@chromium.org3410d912009-06-09 20:56:16 +0000502 # File extensions that are considered source files from a style guide
503 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000504 #
505 # Files without an extension aren't included in the list. If you want to
local_bot30f774e2020-06-25 18:23:34 +0000506 # filter them as source files, add r'(^|.*?[\\\/])[^.]+$' to the allow list.
local_bot64021412020-07-08 21:05:39 +0000507 # Note that ALL CAPS files are skipped in DEFAULT_FILES_TO_SKIP below.
508 DEFAULT_FILES_TO_CHECK = (
maruel@chromium.org3410d912009-06-09 20:56:16 +0000509 # C++ and friends
Edward Lemura5799e32020-01-17 19:26:51 +0000510 r'.+\.c$', r'.+\.cc$', r'.+\.cpp$', r'.+\.h$', r'.+\.m$', r'.+\.mm$',
511 r'.+\.inl$', r'.+\.asm$', r'.+\.hxx$', r'.+\.hpp$', r'.+\.s$', r'.+\.S$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000512 # Scripts
Edward Lemura5799e32020-01-17 19:26:51 +0000513 r'.+\.js$', r'.+\.py$', r'.+\.sh$', r'.+\.rb$', r'.+\.pl$', r'.+\.pm$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000514 # Other
Edward Lemura5799e32020-01-17 19:26:51 +0000515 r'.+\.java$', r'.+\.mk$', r'.+\.am$', r'.+\.css$', r'.+\.mojom$',
516 r'.+\.fidl$'
maruel@chromium.org3410d912009-06-09 20:56:16 +0000517 )
518
519 # Path regexp that should be excluded from being considered containing source
520 # files. Don't modify this list from a presubmit script!
local_bot64021412020-07-08 21:05:39 +0000521 DEFAULT_FILES_TO_SKIP = (
Edward Lemura5799e32020-01-17 19:26:51 +0000522 r'testing_support[\\\/]google_appengine[\\\/].*',
523 r'.*\bexperimental[\\\/].*',
Kent Tamura179dd1e2018-04-26 15:07:41 +0900524 # Exclude third_party/.* but NOT third_party/{WebKit,blink}
525 # (crbug.com/539768 and crbug.com/836555).
Edward Lemura5799e32020-01-17 19:26:51 +0000526 r'.*\bthird_party[\\\/](?!(WebKit|blink)[\\\/]).*',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000527 # Output directories (just in case)
Edward Lemura5799e32020-01-17 19:26:51 +0000528 r'.*\bDebug[\\\/].*',
529 r'.*\bRelease[\\\/].*',
530 r'.*\bxcodebuild[\\\/].*',
531 r'.*\bout[\\\/].*',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000532 # All caps files like README and LICENCE.
Edward Lemura5799e32020-01-17 19:26:51 +0000533 r'.*\b[A-Z0-9_]{2,}$',
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000534 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
Edward Lemura5799e32020-01-17 19:26:51 +0000535 r'(|.*[\\\/])\.git[\\\/].*',
536 r'(|.*[\\\/])\.svn[\\\/].*',
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000537 # There is no point in processing a patch file.
Edward Lemura5799e32020-01-17 19:26:51 +0000538 r'.+\.diff$',
539 r'.+\.patch$',
maruel@chromium.org3410d912009-06-09 20:56:16 +0000540 )
541
local_bot30f774e2020-06-25 18:23:34 +0000542 # TODO(https://crbug.com/1098562): Remove once no longer used
543 @property
544 def DEFAULT_WHITE_LIST(self):
local_bot64021412020-07-08 21:05:39 +0000545 return self.DEFAULT_FILES_TO_CHECK
local_bot30f774e2020-06-25 18:23:34 +0000546
547 # TODO(https://crbug.com/1098562): Remove once no longer used
local_bot37ce2012020-06-26 17:39:24 +0000548 @DEFAULT_WHITE_LIST.setter
549 def DEFAULT_WHITE_LIST(self, value):
local_bot64021412020-07-08 21:05:39 +0000550 self.DEFAULT_FILES_TO_CHECK = value
551
552 # TODO(https://crbug.com/1098562): Remove once no longer used
553 @property
554 def DEFAULT_ALLOW_LIST(self):
555 return self.DEFAULT_FILES_TO_CHECK
556
557 # TODO(https://crbug.com/1098562): Remove once no longer used
558 @DEFAULT_ALLOW_LIST.setter
559 def DEFAULT_ALLOW_LIST(self, value):
560 self.DEFAULT_FILES_TO_CHECK = value
local_bot37ce2012020-06-26 17:39:24 +0000561
562 # TODO(https://crbug.com/1098562): Remove once no longer used
local_bot30f774e2020-06-25 18:23:34 +0000563 @property
564 def DEFAULT_BLACK_LIST(self):
local_bot64021412020-07-08 21:05:39 +0000565 return self.DEFAULT_FILES_TO_SKIP
local_bot30f774e2020-06-25 18:23:34 +0000566
local_bot37ce2012020-06-26 17:39:24 +0000567 # TODO(https://crbug.com/1098562): Remove once no longer used
568 @DEFAULT_BLACK_LIST.setter
569 def DEFAULT_BLACK_LIST(self, value):
local_bot64021412020-07-08 21:05:39 +0000570 self.DEFAULT_FILES_TO_SKIP = value
571
572 # TODO(https://crbug.com/1098562): Remove once no longer used
573 @property
574 def DEFAULT_BLOCK_LIST(self):
575 return self.DEFAULT_FILES_TO_SKIP
576
577 # TODO(https://crbug.com/1098562): Remove once no longer used
578 @DEFAULT_BLOCK_LIST.setter
579 def DEFAULT_BLOCK_LIST(self, value):
580 self.DEFAULT_FILES_TO_SKIP = value
local_bot37ce2012020-06-26 17:39:24 +0000581
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000582 def __init__(self, change, presubmit_path, is_committing,
Edward Lesmes8e282792018-04-03 18:50:29 -0400583 verbose, gerrit_obj, dry_run=None, thread_pool=None, parallel=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000584 """Builds an InputApi object.
585
586 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000587 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000588 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000589 is_committing: True if the change is about to be committed.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000590 gerrit_obj: provides basic Gerrit codereview functionality.
591 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -0400592 parallel: if true, all tests reported via input_api.RunTests for all
593 PRESUBMIT files will be run in parallel.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000594 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000595 # Version number of the presubmit_support script.
596 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000597 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000598 self.is_committing = is_committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000599 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000600 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000601
Edward Lesmes8e282792018-04-03 18:50:29 -0400602 self.parallel = parallel
603 self.thread_pool = thread_pool or ThreadPool()
604
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000605 # We expose various modules and functions as attributes of the input_api
606 # so that presubmit scripts don't have to import them.
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +0900607 self.ast = ast
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000608 self.basename = os.path.basename
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000609 self.cpplint = cpplint
dcheng091b7db2016-06-16 01:27:51 -0700610 self.fnmatch = fnmatch
Yoshisato Yanagisawa04600b42019-03-15 03:03:41 +0000611 self.gclient_paths = gclient_paths
Yoshisato Yanagisawa57dd17b2019-03-22 09:10:29 +0000612 # TODO(yyanagisawa): stop exposing this when python3 become default.
613 # Since python3's tempfile has TemporaryDirectory, we do not need this.
614 self.temporary_directory = gclient_utils.temporary_directory
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000615 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000616 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000617 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000618 self.os_listdir = os.listdir
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000619 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000620 self.os_stat = os.stat
Yoshisato Yanagisawa406de132018-06-29 05:43:25 +0000621 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000622 self.re = re
623 self.subprocess = subprocess
Edward Lemurb9830242019-10-30 22:19:20 +0000624 self.sys = sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000625 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000626 self.time = time
maruel@chromium.org1487d532009-06-06 00:22:57 +0000627 self.unittest = unittest
Edward Lemurb9830242019-10-30 22:19:20 +0000628 if sys.version_info.major == 2:
629 self.urllib2 = urllib2
Edward Lemur16af3562019-10-17 22:11:33 +0000630 self.urllib_request = urllib_request
631 self.urllib_error = urllib_error
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000632
Robert Iannucci50258932018-03-19 10:30:59 -0700633 self.is_windows = sys.platform == 'win32'
634
Edward Lemurb9646622019-10-25 20:57:35 +0000635 # Set python_executable to 'vpython' in order to allow scripts in other
636 # repos (e.g. src.git) to automatically pick up that repo's .vpython file,
637 # instead of inheriting the one in depot_tools.
638 self.python_executable = 'vpython'
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000639 self.environ = os.environ
640
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000641 # InputApi.platform is the platform you're currently running on.
642 self.platform = sys.platform
643
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000644 self.cpu_count = multiprocessing.cpu_count()
645
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000646 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000647 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000648
649 # We carry the canned checks so presubmit scripts can easily use them.
650 self.canned_checks = presubmit_canned_checks
651
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100652 # Temporary files we must manually remove at the end of a run.
653 self._named_temporary_files = []
Jochen Eisinger72606f82017-04-04 10:44:18 +0200654
Edward Lesmes48492322021-03-17 16:47:09 +0000655 # TODO(dpranke): figure out a list of all approved owners for a repo
656 # in order to be able to handle wildcard OWNERS files?
657 self.owners_client = owners_client.DepotToolsClient(
658 change.RepositoryRoot(), change.UpstreamBranch(), os_path=self.os_path)
Edward Lesmes9ce03f82021-01-12 20:13:31 +0000659 self.owners_db = owners_db.Database(
660 change.RepositoryRoot(), fopen=open, os_path=self.os_path)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200661 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000662 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000663 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000664
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000665 # Replace <hash_map> and <hash_set> as headers that need to be included
Edward Lemura5799e32020-01-17 19:26:51 +0000666 # with 'base/containers/hash_tables.h' instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000667 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800668 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000669 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000670 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000671 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
672 for (a, b, header) in cpplint._re_pattern_templates
673 ]
674
Edward Lemurecc27072020-01-06 16:42:34 +0000675 def SetTimeout(self, timeout):
676 self.thread_pool.timeout = timeout
677
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000678 def PresubmitLocalPath(self):
679 """Returns the local path of the presubmit script currently being run.
680
681 This is useful if you don't want to hard-code absolute paths in the
682 presubmit script. For example, It can be used to find another file
683 relative to the PRESUBMIT.py script, so the whole tree can be branched and
684 the presubmit script still works, without editing its content.
685 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000686 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687
agable0b65e732016-11-22 09:25:46 -0800688 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000689 """Same as input_api.change.AffectedFiles() except only lists files
690 (and optionally directories) in the same directory as the current presubmit
Bruce Dawson7a0b07a2020-04-23 17:14:40 +0000691 script, or subdirectories thereof. Note that files are listed using the OS
692 path separator, so backslashes are used as separators on Windows.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000693 """
Edward Lemura5799e32020-01-17 19:26:51 +0000694 dir_with_slash = normpath('%s/' % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000695 if len(dir_with_slash) == 1:
696 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000697
Edward Lemurb9830242019-10-30 22:19:20 +0000698 return list(filter(
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000699 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
Edward Lemurb9830242019-10-30 22:19:20 +0000700 self.change.AffectedFiles(include_deletes, file_filter)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000701
agable0b65e732016-11-22 09:25:46 -0800702 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000703 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800704 paths = [af.LocalPath() for af in self.AffectedFiles()]
Edward Lemura5799e32020-01-17 19:26:51 +0000705 logging.debug('LocalPaths: %s', paths)
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000706 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000707
agable0b65e732016-11-22 09:25:46 -0800708 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800710 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000711
John Budorick16162372018-04-18 10:39:53 -0700712 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
agable0b65e732016-11-22 09:25:46 -0800713 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000714 in the same directory as the current presubmit script, or subdirectories
715 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000716 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000717 if include_deletes is not None:
Edward Lemura5799e32020-01-17 19:26:51 +0000718 warn('AffectedTestableFiles(include_deletes=%s)'
719 ' is deprecated and ignored' % str(include_deletes),
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000720 category=DeprecationWarning,
721 stacklevel=2)
Edward Lemurb9830242019-10-30 22:19:20 +0000722 return list(filter(
723 lambda x: x.IsTestableFile(),
724 self.AffectedFiles(include_deletes=False, **kwargs)))
agable0b65e732016-11-22 09:25:46 -0800725
726 def AffectedTextFiles(self, include_deletes=None):
727 """An alias to AffectedTestableFiles for backwards compatibility."""
728 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000729
Josip Sokcevic8c955952021-02-01 21:32:57 +0000730 def FilterSourceFile(self,
731 affected_file,
732 files_to_check=None,
733 files_to_skip=None,
734 allow_list=None,
735 block_list=None):
Edward Lemura5799e32020-01-17 19:26:51 +0000736 """Filters out files that aren't considered 'source file'.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000737
local_bot64021412020-07-08 21:05:39 +0000738 If files_to_check or files_to_skip is None, InputApi.DEFAULT_FILES_TO_CHECK
739 and InputApi.DEFAULT_FILES_TO_SKIP is used respectively.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000740
741 The lists will be compiled as regular expression and
742 AffectedFile.LocalPath() needs to pass both list.
743
744 Note: Copy-paste this function to suit your needs or use a lambda function.
745 """
local_bot64021412020-07-08 21:05:39 +0000746 if files_to_check is None:
747 files_to_check = self.DEFAULT_FILES_TO_CHECK
748 if files_to_skip is None:
749 files_to_skip = self.DEFAULT_FILES_TO_SKIP
local_bot30f774e2020-06-25 18:23:34 +0000750
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000751 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000752 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000753 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000754 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000755 return True
756 return False
local_bot64021412020-07-08 21:05:39 +0000757 return (Find(affected_file, files_to_check) and
758 not Find(affected_file, files_to_skip))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000759
760 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800761 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000762
763 If source_file is None, InputApi.FilterSourceFile() is used.
764 """
765 if not source_file:
766 source_file = self.FilterSourceFile
Edward Lemurb9830242019-10-30 22:19:20 +0000767 return list(filter(source_file, self.AffectedTestableFiles()))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000768
769 def RightHandSideLines(self, source_file_filter=None):
Edward Lemura5799e32020-01-17 19:26:51 +0000770 """An iterator over all text lines in 'new' version of changed files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000771
772 Only lists lines from new or modified text files in the change that are
773 contained by the directory of the currently executing presubmit script.
774
775 This is useful for doing line-by-line regex checks, like checking for
776 trailing whitespace.
777
778 Yields:
779 a 3 tuple:
780 the AffectedFile instance of the current file;
781 integer line number (1-based); and
782 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000783
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000784 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000785 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000786 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000787 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000788
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000789 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000790 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000791
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000792 Deny reading anything outside the repository.
793 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000794 if isinstance(file_item, AffectedFile):
795 file_item = file_item.AbsoluteLocalPath()
796 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000797 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000798 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000799
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100800 def CreateTemporaryFile(self, **kwargs):
801 """Returns a named temporary file that must be removed with a call to
802 RemoveTemporaryFiles().
803
804 All keyword arguments are forwarded to tempfile.NamedTemporaryFile(),
805 except for |delete|, which is always set to False.
806
807 Presubmit checks that need to create a temporary file and pass it for
808 reading should use this function instead of NamedTemporaryFile(), as
809 Windows fails to open a file that is already open for writing.
810
811 with input_api.CreateTemporaryFile() as f:
812 f.write('xyz')
813 f.close()
814 input_api.subprocess.check_output(['script-that', '--reads-from',
815 f.name])
816
817
818 Note that callers of CreateTemporaryFile() should not worry about removing
819 any temporary file; this is done transparently by the presubmit handling
820 code.
821 """
822 if 'delete' in kwargs:
823 # Prevent users from passing |delete|; we take care of file deletion
824 # ourselves and this prevents unintuitive error messages when we pass
825 # delete=False and 'delete' is also in kwargs.
826 raise TypeError('CreateTemporaryFile() does not take a "delete" '
827 'argument, file deletion is handled automatically by '
828 'the same presubmit_support code that creates InputApi '
829 'objects.')
830 temp_file = self.tempfile.NamedTemporaryFile(delete=False, **kwargs)
831 self._named_temporary_files.append(temp_file.name)
832 return temp_file
833
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000834 @property
835 def tbr(self):
836 """Returns if a change is TBR'ed."""
Jeremy Romandce22502017-06-20 15:37:29 -0400837 return 'TBR' in self.change.tags or self.change.TBRsFromDescription()
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000838
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000839 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000840 tests = []
841 msgs = []
842 for t in tests_mix:
Edward Lesmes8e282792018-04-03 18:50:29 -0400843 if isinstance(t, OutputApi.PresubmitResult) and t:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000844 msgs.append(t)
845 else:
846 assert issubclass(t.message, _PresubmitResult)
847 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000848 if self.verbose:
849 t.info = _PresubmitNotifyResult
Edward Lemur1037c742018-05-01 18:56:04 -0400850 if not t.kwargs.get('cwd'):
851 t.kwargs['cwd'] = self.PresubmitLocalPath()
Edward Lesmes8e282792018-04-03 18:50:29 -0400852 self.thread_pool.AddTests(tests, parallel)
Edward Lemur21000eb2019-05-24 23:25:58 +0000853 # When self.parallel is True (i.e. --parallel is passed as an option)
854 # RunTests doesn't actually run tests. It adds them to a ThreadPool that
855 # will run all tests once all PRESUBMIT files are processed.
856 # Otherwise, it will run them and return the results.
857 if not self.parallel:
Edward Lesmes8e282792018-04-03 18:50:29 -0400858 msgs.extend(self.thread_pool.RunAsync())
859 return msgs
scottmg86099d72016-09-01 09:16:51 -0700860
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000861
nick@chromium.orgff526192013-06-10 19:30:26 +0000862class _DiffCache(object):
863 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000864 def __init__(self, upstream=None):
865 """Stores the upstream revision against which all diffs will be computed."""
866 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000867
868 def GetDiff(self, path, local_root):
869 """Get the diff for a particular path."""
870 raise NotImplementedError()
871
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700872 def GetOldContents(self, path, local_root):
873 """Get the old version for a particular path."""
874 raise NotImplementedError()
875
nick@chromium.orgff526192013-06-10 19:30:26 +0000876
nick@chromium.orgff526192013-06-10 19:30:26 +0000877class _GitDiffCache(_DiffCache):
878 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000879 def __init__(self, upstream):
880 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000881 self._diffs_by_file = None
882
883 def GetDiff(self, path, local_root):
884 if not self._diffs_by_file:
885 # Compute a single diff for all files and parse the output; should
886 # with git this is much faster than computing one diff for each file.
887 diffs = {}
888
889 # Don't specify any filenames below, because there are command line length
890 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000891 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
892 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000893
894 # This regex matches the path twice, separated by a space. Note that
895 # filename itself may contain spaces.
896 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
897 current_diff = []
898 keep_line_endings = True
899 for x in unified_diff.splitlines(keep_line_endings):
900 match = file_marker.match(x)
901 if match:
902 # Marks the start of a new per-file section.
903 diffs[match.group('filename')] = current_diff = [x]
904 elif x.startswith('diff --git'):
905 raise PresubmitFailure('Unexpected diff line: %s' % x)
906 else:
907 current_diff.append(x)
908
909 self._diffs_by_file = dict(
910 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
911
912 if path not in self._diffs_by_file:
913 raise PresubmitFailure(
914 'Unified diff did not contain entry for file %s' % path)
915
916 return self._diffs_by_file[path]
917
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700918 def GetOldContents(self, path, local_root):
919 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
920
nick@chromium.orgff526192013-06-10 19:30:26 +0000921
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000922class AffectedFile(object):
923 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000924
925 DIFF_CACHE = _DiffCache
926
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000927 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800928 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000929 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000930 self._path = path
931 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000932 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000933 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000934 self._cached_changed_contents = None
935 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000936 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700937 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000938
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000939 def LocalPath(self):
940 """Returns the path of this file on the local disk relative to client root.
Andrew Grieve92b8b992017-11-02 09:42:24 -0400941
942 This should be used for error messages but not for accessing files,
943 because presubmit checks are run with CWD=PresubmitLocalPath() (which is
944 often != client root).
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000945 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000946 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000947
948 def AbsoluteLocalPath(self):
949 """Returns the absolute path of this file on the local disk.
950 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000951 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000952
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000953 def Action(self):
954 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000955 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000956
agable0b65e732016-11-22 09:25:46 -0800957 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000958 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000959
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000960 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000961 raise NotImplementedError() # Implement when needed
962
agable0b65e732016-11-22 09:25:46 -0800963 def IsTextFile(self):
964 """An alias to IsTestableFile for backwards compatibility."""
965 return self.IsTestableFile()
966
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700967 def OldContents(self):
968 """Returns an iterator over the lines in the old version of file.
969
Daniel Cheng2da34fe2017-03-21 20:42:12 -0700970 The old version is the file before any modifications in the user's
Edward Lemura5799e32020-01-17 19:26:51 +0000971 workspace, i.e. the 'left hand side'.
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700972
973 Contents will be empty if the file is a directory or does not exist.
974 Note: The carriage returns (LF or CR) are stripped off.
975 """
976 return self._diff_cache.GetOldContents(self.LocalPath(),
977 self._local_root).splitlines()
978
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000979 def NewContents(self):
980 """Returns an iterator over the lines in the new version of file.
981
Edward Lemura5799e32020-01-17 19:26:51 +0000982 The new version is the file in the user's workspace, i.e. the 'right hand
983 side'.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984
985 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000986 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000987 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000988 if self._cached_new_contents is None:
989 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800990 try:
991 self._cached_new_contents = gclient_utils.FileRead(
992 self.AbsoluteLocalPath(), 'rU').splitlines()
993 except IOError:
994 pass # File not found? That's fine; maybe it was deleted.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000995 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000996
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000997 def ChangedContents(self):
998 """Returns a list of tuples (line number, line text) of all new lines.
999
1000 This relies on the scm diff output describing each changed code section
1001 with a line of the form
1002
1003 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
1004 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +00001005 if self._cached_changed_contents is not None:
1006 return self._cached_changed_contents[:]
1007 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001008 line_num = 0
1009
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001010 for line in self.GenerateScmDiff().splitlines():
Edward Lemurac5c55f2020-02-29 00:17:16 +00001011 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
1012 if m:
1013 line_num = int(m.groups(1)[0])
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001014 continue
1015 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +00001016 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001017 if not line.startswith('-'):
1018 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +00001019 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001020
maruel@chromium.org5de13972009-06-10 18:16:06 +00001021 def __str__(self):
1022 return self.LocalPath()
1023
maruel@chromium.orgab05d582011-02-09 23:41:22 +00001024 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +00001025 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001026
maruel@chromium.org58407af2011-04-12 23:15:57 +00001027
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001028class GitAffectedFile(AffectedFile):
1029 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +00001030 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001031 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001032
nick@chromium.orgff526192013-06-10 19:30:26 +00001033 DIFF_CACHE = _GitDiffCache
1034
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001035 def __init__(self, *args, **kwargs):
1036 AffectedFile.__init__(self, *args, **kwargs)
1037 self._server_path = None
agable0b65e732016-11-22 09:25:46 -08001038 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001039
agable0b65e732016-11-22 09:25:46 -08001040 def IsTestableFile(self):
1041 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001042 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -08001043 # A deleted file is not testable.
1044 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001045 else:
agable0b65e732016-11-22 09:25:46 -08001046 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
1047 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001048
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001049
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001050class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001051 """Describe a change.
1052
1053 Used directly by the presubmit scripts to query the current change being
1054 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001055
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001056 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +00001057 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001058 self.KEY: equivalent to tags['KEY']
1059 """
1060
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001061 _AFFECTED_FILES = AffectedFile
1062
Edward Lemura5799e32020-01-17 19:26:51 +00001063 # Matches key/value (or 'tag') lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +00001064 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001065 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001066 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001067
maruel@chromium.org58407af2011-04-12 23:15:57 +00001068 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001069 self, name, description, local_root, files, issue, patchset, author,
1070 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001071 if files is None:
1072 files = []
1073 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +00001074 # Convert root into an absolute path.
1075 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001076 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001077 self.issue = issue
1078 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +00001079 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001080
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001081 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001082 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001083 self._description_without_tags = ''
1084 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001085
maruel@chromium.orge085d812011-10-10 19:49:15 +00001086 assert all(
1087 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
1088
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001089 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001090 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +00001091 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
1092 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +00001093 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001094
Edward Lesmes9ce03f82021-01-12 20:13:31 +00001095 def UpstreamBranch(self):
1096 """Returns the upstream branch for the change."""
1097 return self._upstream
1098
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001099 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001100 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001101 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001102
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001103 def DescriptionText(self):
1104 """Returns the user-entered changelist description, minus tags.
1105
Edward Lemura5799e32020-01-17 19:26:51 +00001106 Any line in the user-provided description starting with e.g. 'FOO='
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001107 (whitespace permitted before and around) is considered a tag line. Such
1108 lines are stripped out of the description this function returns.
1109 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001110 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001111
1112 def FullDescriptionText(self):
1113 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001114 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001115
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001116 def SetDescriptionText(self, description):
1117 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001118
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001119 Also updates the list of tags."""
1120 self._full_description = description
1121
1122 # From the description text, build up a dictionary of key/value pairs
Edward Lemura5799e32020-01-17 19:26:51 +00001123 # plus the description minus all key/value or 'tag' lines.
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001124 description_without_tags = []
1125 self.tags = {}
1126 for line in self._full_description.splitlines():
1127 m = self.TAG_LINE_RE.match(line)
1128 if m:
1129 self.tags[m.group('key')] = m.group('value')
1130 else:
1131 description_without_tags.append(line)
1132
1133 # Change back to text and remove whitespace at end.
1134 self._description_without_tags = (
1135 '\n'.join(description_without_tags).rstrip())
1136
Edward Lemur69bb8be2020-02-03 20:37:38 +00001137 def AddDescriptionFooter(self, key, value):
1138 """Adds the given footer to the change description.
1139
1140 Args:
1141 key: A string with the key for the git footer. It must conform to
1142 the git footers format (i.e. 'List-Of-Tokens') and will be case
1143 normalized so that each token is title-cased.
1144 value: A string with the value for the git footer.
1145 """
1146 description = git_footers.add_footer(
1147 self.FullDescriptionText(), git_footers.normalize_name(key), value)
1148 self.SetDescriptionText(description)
1149
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001150 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001151 """Returns the repository (checkout) root directory for this change,
1152 as an absolute path.
1153 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001154 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001155
1156 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001157 """Return tags directly as attributes on the object."""
Edward Lemura5799e32020-01-17 19:26:51 +00001158 if not re.match(r'^[A-Z_]*$', attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001159 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +00001160 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001161
Edward Lemur69bb8be2020-02-03 20:37:38 +00001162 def GitFootersFromDescription(self):
1163 """Return the git footers present in the description.
1164
1165 Returns:
1166 footers: A dict of {footer: [values]} containing a multimap of the footers
1167 in the change description.
1168 """
1169 return git_footers.parse_footers(self.FullDescriptionText())
1170
Aaron Gablefc03e672017-05-15 14:09:42 -07001171 def BugsFromDescription(self):
1172 """Returns all bugs referenced in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -07001173 tags = [b.strip() for b in self.tags.get('BUG', '').split(',') if b.strip()]
Caleb Rouleauc0546b92019-02-22 06:12:57 +00001174 footers = []
Edward Lemur69bb8be2020-02-03 20:37:38 +00001175 parsed = self.GitFootersFromDescription()
Dan Beam62954042019-10-03 21:20:33 +00001176 unsplit_footers = parsed.get('Bug', []) + parsed.get('Fixed', [])
Caleb Rouleauc0546b92019-02-22 06:12:57 +00001177 for unsplit_footer in unsplit_footers:
1178 footers += [b.strip() for b in unsplit_footer.split(',')]
Aaron Gable12ef5012017-05-15 14:29:00 -07001179 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -07001180
1181 def ReviewersFromDescription(self):
1182 """Returns all reviewers listed in the commit description."""
Edward Lemura5799e32020-01-17 19:26:51 +00001183 # We don't support a 'R:' git-footer for reviewers; that is in metadata.
Aaron Gable12ef5012017-05-15 14:29:00 -07001184 tags = [r.strip() for r in self.tags.get('R', '').split(',') if r.strip()]
1185 return sorted(set(tags))
Aaron Gablefc03e672017-05-15 14:09:42 -07001186
1187 def TBRsFromDescription(self):
1188 """Returns all TBR reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -07001189 tags = [r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()]
Aaron Gable6e7ddb62020-05-27 22:23:29 +00001190 # TODO(crbug.com/839208): Remove support for 'Tbr:' when TBRs are
1191 # programmatically determined by self-CR+1s.
Edward Lemur69bb8be2020-02-03 20:37:38 +00001192 footers = self.GitFootersFromDescription().get('Tbr', [])
Aaron Gable12ef5012017-05-15 14:29:00 -07001193 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -07001194
Aaron Gable6e7ddb62020-05-27 22:23:29 +00001195 # TODO(crbug.com/753425): Delete these once we're sure they're unused.
Aaron Gablefc03e672017-05-15 14:09:42 -07001196 @property
1197 def BUG(self):
1198 return ','.join(self.BugsFromDescription())
1199 @property
1200 def R(self):
1201 return ','.join(self.ReviewersFromDescription())
1202 @property
1203 def TBR(self):
1204 return ','.join(self.TBRsFromDescription())
1205
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001206 def AllFiles(self, root=None):
1207 """List all files under source control in the repo."""
1208 raise NotImplementedError()
1209
agable0b65e732016-11-22 09:25:46 -08001210 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001211 """Returns a list of AffectedFile instances for all files in the change.
1212
1213 Args:
1214 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +00001215 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001216
1217 Returns:
1218 [AffectedFile(path, action), AffectedFile(path, action)]
1219 """
Edward Lemurb9830242019-10-30 22:19:20 +00001220 affected = list(filter(file_filter, self._affected_files))
sail@chromium.org5538e022011-05-12 17:53:16 +00001221
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001222 if include_deletes:
1223 return affected
Edward Lemurb9830242019-10-30 22:19:20 +00001224 return list(filter(lambda x: x.Action() != 'D', affected))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001225
John Budorick16162372018-04-18 10:39:53 -07001226 def AffectedTestableFiles(self, include_deletes=None, **kwargs):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +00001227 """Return a list of the existing text files in a change."""
1228 if include_deletes is not None:
Edward Lemura5799e32020-01-17 19:26:51 +00001229 warn('AffectedTeestableFiles(include_deletes=%s)'
1230 ' is deprecated and ignored' % str(include_deletes),
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001231 category=DeprecationWarning,
1232 stacklevel=2)
Edward Lemurb9830242019-10-30 22:19:20 +00001233 return list(filter(
1234 lambda x: x.IsTestableFile(),
1235 self.AffectedFiles(include_deletes=False, **kwargs)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001236
agable0b65e732016-11-22 09:25:46 -08001237 def AffectedTextFiles(self, include_deletes=None):
1238 """An alias to AffectedTestableFiles for backwards compatibility."""
1239 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001240
agable0b65e732016-11-22 09:25:46 -08001241 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001242 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001243 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001244
agable0b65e732016-11-22 09:25:46 -08001245 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001246 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001247 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001248
1249 def RightHandSideLines(self):
Edward Lemura5799e32020-01-17 19:26:51 +00001250 """An iterator over all text lines in 'new' version of changed files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001251
1252 Lists lines from new or modified text files in the change.
1253
1254 This is useful for doing line-by-line regex checks, like checking for
1255 trailing whitespace.
1256
1257 Yields:
1258 a 3 tuple:
1259 the AffectedFile instance of the current file;
1260 integer line number (1-based); and
1261 the contents of the line as a string.
1262 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001263 return _RightHandSideLinesImpl(
1264 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -08001265 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001266
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02001267 def OriginalOwnersFiles(self):
1268 """A map from path names of affected OWNERS files to their old content."""
1269 def owners_file_filter(f):
1270 return 'OWNERS' in os.path.split(f.LocalPath())[1]
1271 files = self.AffectedFiles(file_filter=owners_file_filter)
1272 return dict([(f.LocalPath(), f.OldContents()) for f in files])
1273
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001274
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001275class GitChange(Change):
1276 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001277 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001278
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001279 def AllFiles(self, root=None):
1280 """List all files under source control in the repo."""
1281 root = root or self.RepositoryRoot()
1282 return subprocess.check_output(
Aaron Gable7817f022017-12-12 09:43:17 -08001283 ['git', '-c', 'core.quotePath=false', 'ls-files', '--', '.'],
1284 cwd=root).splitlines()
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001285
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001286
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001287def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001288 """Finds all presubmit files that apply to a given set of source files.
1289
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001290 If inherit-review-settings-ok is present right under root, looks for
1291 PRESUBMIT.py in directories enclosing root.
1292
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001293 Args:
1294 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001295 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001296
1297 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001298 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001299 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001300 files = [normpath(os.path.join(root, f)) for f in files]
1301
1302 # List all the individual directories containing files.
1303 directories = set([os.path.dirname(f) for f in files])
1304
1305 # Ignore root if inherit-review-settings-ok is present.
1306 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1307 root = None
1308
1309 # Collect all unique directories that may contain PRESUBMIT.py.
1310 candidates = set()
1311 for directory in directories:
1312 while True:
1313 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001314 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001315 candidates.add(directory)
1316 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001317 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001318 parent_dir = os.path.dirname(directory)
1319 if parent_dir == directory:
1320 # We hit the system root directory.
1321 break
1322 directory = parent_dir
1323
1324 # Look for PRESUBMIT.py in all candidate directories.
1325 results = []
1326 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -07001327 try:
1328 for f in os.listdir(directory):
1329 p = os.path.join(directory, f)
1330 if os.path.isfile(p) and re.match(
1331 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1332 results.append(p)
1333 except OSError:
1334 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001335
tobiasjs2836bcf2016-08-16 04:08:16 -07001336 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001337 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001338
1339
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001340class GetTryMastersExecuter(object):
1341 @staticmethod
1342 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1343 """Executes GetPreferredTryMasters() from a single presubmit script.
1344
1345 Args:
1346 script_text: The text of the presubmit script.
1347 presubmit_path: Project script to run.
1348 project: Project name to pass to presubmit script for bot selection.
1349
1350 Return:
1351 A map of try masters to map of builders to set of tests.
1352 """
1353 context = {}
1354 try:
Raul Tambre09e64b42019-05-14 01:57:22 +00001355 exec(compile(script_text, 'PRESUBMIT.py', 'exec', dont_inherit=True),
1356 context)
Raul Tambre7c938462019-05-24 16:35:35 +00001357 except Exception as e:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001358 raise PresubmitFailure('"%s" had an exception.\n%s'
1359 % (presubmit_path, e))
1360
1361 function_name = 'GetPreferredTryMasters'
1362 if function_name not in context:
1363 return {}
1364 get_preferred_try_masters = context[function_name]
1365 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1366 raise PresubmitFailure(
1367 'Expected function "GetPreferredTryMasters" to take two arguments.')
1368 return get_preferred_try_masters(project, change)
1369
1370
rmistry@google.com5626a922015-02-26 14:03:30 +00001371class GetPostUploadExecuter(object):
1372 @staticmethod
Edward Lemur016a0872020-02-04 22:13:28 +00001373 def ExecPresubmitScript(script_text, presubmit_path, gerrit_obj, change):
rmistry@google.com5626a922015-02-26 14:03:30 +00001374 """Executes PostUploadHook() from a single presubmit script.
1375
1376 Args:
1377 script_text: The text of the presubmit script.
1378 presubmit_path: Project script to run.
Edward Lemur016a0872020-02-04 22:13:28 +00001379 gerrit_obj: The GerritAccessor object.
rmistry@google.com5626a922015-02-26 14:03:30 +00001380 change: The Change object.
1381
1382 Return:
1383 A list of results objects.
1384 """
1385 context = {}
1386 try:
Raul Tambre09e64b42019-05-14 01:57:22 +00001387 exec(compile(script_text, 'PRESUBMIT.py', 'exec', dont_inherit=True),
1388 context)
Raul Tambre7c938462019-05-24 16:35:35 +00001389 except Exception as e:
rmistry@google.com5626a922015-02-26 14:03:30 +00001390 raise PresubmitFailure('"%s" had an exception.\n%s'
1391 % (presubmit_path, e))
1392
1393 function_name = 'PostUploadHook'
1394 if function_name not in context:
1395 return {}
1396 post_upload_hook = context[function_name]
1397 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1398 raise PresubmitFailure(
1399 'Expected function "PostUploadHook" to take three arguments.')
Edward Lemur016a0872020-02-04 22:13:28 +00001400 return post_upload_hook(gerrit_obj, change, OutputApi(False))
rmistry@google.com5626a922015-02-26 14:03:30 +00001401
1402
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001403def _MergeMasters(masters1, masters2):
1404 """Merges two master maps. Merges also the tests of each builder."""
1405 result = {}
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001406 for (master, builders) in itertools.chain(masters1.items(),
1407 masters2.items()):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001408 new_builders = result.setdefault(master, {})
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001409 for (builder, tests) in builders.items():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001410 new_builders.setdefault(builder, set([])).update(tests)
1411 return result
1412
1413
1414def DoGetTryMasters(change,
1415 changed_files,
1416 repository_root,
1417 default_presubmit,
1418 project,
1419 verbose,
1420 output_stream):
1421 """Get the list of try masters from the presubmit scripts.
1422
1423 Args:
1424 changed_files: List of modified files.
1425 repository_root: The repository root.
1426 default_presubmit: A default presubmit script to execute in any case.
1427 project: Optional name of a project used in selecting trybots.
1428 verbose: Prints debug info.
1429 output_stream: A stream to write debug output to.
1430
1431 Return:
1432 Map of try masters to map of builders to set of tests.
1433 """
1434 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1435 if not presubmit_files and verbose:
Edward Lemura5799e32020-01-17 19:26:51 +00001436 output_stream.write('Warning, no PRESUBMIT.py found.\n')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001437 results = {}
1438 executer = GetTryMastersExecuter()
1439
1440 if default_presubmit:
1441 if verbose:
Edward Lemura5799e32020-01-17 19:26:51 +00001442 output_stream.write('Running default presubmit script.\n')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001443 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1444 results = _MergeMasters(results, executer.ExecPresubmitScript(
1445 default_presubmit, fake_path, project, change))
1446 for filename in presubmit_files:
1447 filename = os.path.abspath(filename)
1448 if verbose:
Edward Lemura5799e32020-01-17 19:26:51 +00001449 output_stream.write('Running %s\n' % filename)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001450 # Accept CRLF presubmit script.
1451 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1452 results = _MergeMasters(results, executer.ExecPresubmitScript(
1453 presubmit_script, filename, project, change))
1454
1455 # Make sets to lists again for later JSON serialization.
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001456 for builders in results.values():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001457 for builder in builders:
1458 builders[builder] = list(builders[builder])
1459
1460 if results and verbose:
1461 output_stream.write('%s\n' % str(results))
1462 return results
1463
1464
rmistry@google.com5626a922015-02-26 14:03:30 +00001465def DoPostUploadExecuter(change,
Edward Lemur016a0872020-02-04 22:13:28 +00001466 gerrit_obj,
Edward Lemur6eb1d322020-02-27 22:20:15 +00001467 verbose):
rmistry@google.com5626a922015-02-26 14:03:30 +00001468 """Execute the post upload hook.
1469
1470 Args:
1471 change: The Change object.
Edward Lemur016a0872020-02-04 22:13:28 +00001472 gerrit_obj: The GerritAccessor object.
rmistry@google.com5626a922015-02-26 14:03:30 +00001473 verbose: Prints debug info.
rmistry@google.com5626a922015-02-26 14:03:30 +00001474 """
1475 presubmit_files = ListRelevantPresubmitFiles(
Edward Lemur6eb1d322020-02-27 22:20:15 +00001476 change.LocalPaths(), change.RepositoryRoot())
rmistry@google.com5626a922015-02-26 14:03:30 +00001477 if not presubmit_files and verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001478 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
rmistry@google.com5626a922015-02-26 14:03:30 +00001479 results = []
1480 executer = GetPostUploadExecuter()
1481 # The root presubmit file should be executed after the ones in subdirectories.
1482 # i.e. the specific post upload hooks should run before the general ones.
1483 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1484 presubmit_files.reverse()
1485
1486 for filename in presubmit_files:
1487 filename = os.path.abspath(filename)
1488 if verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001489 sys.stdout.write('Running %s\n' % filename)
rmistry@google.com5626a922015-02-26 14:03:30 +00001490 # Accept CRLF presubmit script.
1491 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1492 results.extend(executer.ExecPresubmitScript(
Edward Lemur016a0872020-02-04 22:13:28 +00001493 presubmit_script, filename, gerrit_obj, change))
rmistry@google.com5626a922015-02-26 14:03:30 +00001494
Edward Lemur6eb1d322020-02-27 22:20:15 +00001495 if not results:
1496 return 0
1497
1498 sys.stdout.write('\n')
1499 sys.stdout.write('** Post Upload Hook Messages **\n')
1500
1501 exit_code = 0
1502 for result in results:
1503 if result.fatal:
1504 exit_code = 1
1505 result.handle()
1506 sys.stdout.write('\n')
1507
1508 return exit_code
rmistry@google.com5626a922015-02-26 14:03:30 +00001509
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001510class PresubmitExecuter(object):
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001511 def __init__(self, change, committing, verbose, gerrit_obj, dry_run=None,
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001512 thread_pool=None, parallel=False):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001513 """
1514 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001515 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001516 committing: True if 'git cl land' is running, False if 'git cl upload' is.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001517 gerrit_obj: provides basic Gerrit codereview functionality.
1518 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001519 parallel: if true, all tests reported via input_api.RunTests for all
1520 PRESUBMIT files will be run in parallel.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001521 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001522 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001523 self.committing = committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001524 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001525 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001526 self.dry_run = dry_run
Daniel Cheng7227d212017-11-17 08:12:37 -08001527 self.more_cc = []
Edward Lesmes8e282792018-04-03 18:50:29 -04001528 self.thread_pool = thread_pool
1529 self.parallel = parallel
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001530
1531 def ExecPresubmitScript(self, script_text, presubmit_path):
1532 """Executes a single presubmit script.
1533
1534 Args:
1535 script_text: The text of the presubmit script.
1536 presubmit_path: The path to the presubmit file (this will be reported via
1537 input_api.PresubmitLocalPath()).
1538
1539 Return:
1540 A list of result objects, empty if no problems.
1541 """
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001542
chase@chromium.org8e416c82009-10-06 04:30:44 +00001543 # Change to the presubmit file's directory to support local imports.
1544 main_path = os.getcwd()
Saagar Sanghavi98b332f2020-07-31 17:19:15 +00001545 presubmit_dir = os.path.dirname(presubmit_path)
1546 os.chdir(presubmit_dir)
chase@chromium.org8e416c82009-10-06 04:30:44 +00001547
1548 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001549 input_api = InputApi(self.change, presubmit_path, self.committing,
Aaron Gable668c1d82018-04-03 10:19:16 -07001550 self.verbose, gerrit_obj=self.gerrit,
Edward Lesmes8e282792018-04-03 18:50:29 -04001551 dry_run=self.dry_run, thread_pool=self.thread_pool,
1552 parallel=self.parallel)
Daniel Cheng7227d212017-11-17 08:12:37 -08001553 output_api = OutputApi(self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001554 context = {}
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001555
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001556 try:
Raul Tambre09e64b42019-05-14 01:57:22 +00001557 exec(compile(script_text, 'PRESUBMIT.py', 'exec', dont_inherit=True),
1558 context)
Raul Tambre7c938462019-05-24 16:35:35 +00001559 except Exception as e:
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001560 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001561
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001562 context['__args'] = (input_api, output_api)
Ben Pastene8351dc12020-08-06 05:01:35 +00001563
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001564 # Get path of presubmit directory relative to repository root.
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001565 # Always use forward slashes, so that path is same in *nix and Windows
1566 root = input_api.change.RepositoryRoot()
1567 rel_path = os.path.relpath(presubmit_dir, root)
1568 rel_path = rel_path.replace(os.path.sep, '/')
Ben Pastene8351dc12020-08-06 05:01:35 +00001569
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001570 # Get the URL of git remote origin and use it to identify host and project
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001571 host = project = ''
1572 if self.gerrit:
1573 host = self.gerrit.host or ''
1574 project = self.gerrit.project or ''
Saagar Sanghavi531d9922020-08-10 20:14:01 +00001575
1576 # Prefix for test names
1577 prefix = 'presubmit:%s/%s:%s/' % (host, project, rel_path)
1578
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001579 # Perform all the desired presubmit checks.
1580 results = []
Ben Pastene8351dc12020-08-06 05:01:35 +00001581
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001582 try:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001583 version = [
1584 int(x) for x in context.get('PRESUBMIT_VERSION', '0.0.0').split('.')
1585 ]
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001586
Scott Leecc2fe9b2020-11-19 19:38:06 +00001587 with rdb_wrapper.client(prefix) as sink:
1588 if version >= [2, 0, 0]:
1589 for function_name in context:
1590 if not function_name.startswith('Check'):
1591 continue
1592 if function_name.endswith('Commit') and not self.committing:
1593 continue
1594 if function_name.endswith('Upload') and self.committing:
1595 continue
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001596 logging.debug('Running %s in %s', function_name, presubmit_path)
1597 results.extend(
Scott Leecc2fe9b2020-11-19 19:38:06 +00001598 self._run_check_function(function_name, context, sink))
1599 logging.debug('Running %s done.', function_name)
1600 self.more_cc.extend(output_api.more_cc)
1601
1602 else: # Old format
1603 if self.committing:
1604 function_name = 'CheckChangeOnCommit'
1605 else:
1606 function_name = 'CheckChangeOnUpload'
1607 if function_name in context:
1608 logging.debug('Running %s in %s', function_name, presubmit_path)
1609 results.extend(
1610 self._run_check_function(function_name, context, sink))
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001611 logging.debug('Running %s done.', function_name)
1612 self.more_cc.extend(output_api.more_cc)
1613
1614 finally:
1615 for f in input_api._named_temporary_files:
1616 os.remove(f)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001617
chase@chromium.org8e416c82009-10-06 04:30:44 +00001618 # Return the process to the original working directory.
1619 os.chdir(main_path)
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001620 return results
1621
Scott Leecc2fe9b2020-11-19 19:38:06 +00001622 def _run_check_function(self, function_name, context, sink=None):
1623 """Evaluates and returns the result of a given presubmit function.
1624
1625 If sink is given, the result of the presubmit function will be reported
1626 to the ResultSink.
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001627
1628 Args:
Scott Leecc2fe9b2020-11-19 19:38:06 +00001629 function_name: the name of the presubmit function to evaluate
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001630 context: a context dictionary in which the function will be evaluated
Scott Leecc2fe9b2020-11-19 19:38:06 +00001631 sink: an instance of ResultSink. None, by default.
1632 Returns:
1633 the result of the presubmit function call.
1634 """
1635 start_time = time_time()
1636 try:
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001637 result = eval(function_name + '(*__args)', context)
1638 self._check_result_type(result)
Scott Leecc2fe9b2020-11-19 19:38:06 +00001639 except:
1640 if sink:
1641 elapsed_time = time_time() - start_time
1642 sink.report(function_name, rdb_wrapper.STATUS_FAIL, elapsed_time)
1643 raise
1644
1645 if sink:
1646 elapsed_time = time_time() - start_time
1647 status = rdb_wrapper.STATUS_PASS
1648 if any(r.fatal for r in result):
1649 status = rdb_wrapper.STATUS_FAIL
1650 sink.report(function_name, status, elapsed_time)
1651
1652 return result
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001653
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001654 def _check_result_type(self, result):
1655 """Helper function which ensures result is a list, and all elements are
1656 instances of OutputApi.PresubmitResult"""
1657 if not isinstance(result, (tuple, list)):
1658 raise PresubmitFailure('Presubmit functions must return a tuple or list')
1659 if not all(isinstance(res, OutputApi.PresubmitResult) for res in result):
1660 raise PresubmitFailure(
1661 'All presubmit results must be of types derived from '
1662 'output_api.PresubmitResult')
1663
1664
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001665def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001666 committing,
1667 verbose,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001668 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001669 may_prompt,
Aaron Gable668c1d82018-04-03 10:19:16 -07001670 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04001671 dry_run=None,
Debrian Figueroadd2737e2019-06-21 23:50:13 +00001672 parallel=False,
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001673 json_output=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001674 """Runs all presubmit checks that apply to the files in the change.
1675
1676 This finds all PRESUBMIT.py files in directories enclosing the files in the
1677 change (up to the repository root) and calls the relevant entrypoint function
1678 depending on whether the change is being committed or uploaded.
1679
1680 Prints errors, warnings and notifications. Prompts the user for warnings
1681 when needed.
1682
1683 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001684 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001685 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001686 verbose: Prints debug info.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001687 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001688 may_prompt: Enable (y/n) questions on warning or error. If False,
1689 any questions are answered with yes by default.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001690 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001691 dry_run: if true, some Checks will be skipped.
Edward Lesmes8e282792018-04-03 18:50:29 -04001692 parallel: if true, all tests specified by input_api.RunTests in all
1693 PRESUBMIT files will be run in parallel.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001694
1695 Return:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001696 1 if presubmit checks failed or 0 otherwise.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001697 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001698 old_environ = os.environ
1699 try:
1700 # Make sure python subprocesses won't generate .pyc files.
1701 os.environ = os.environ.copy()
Edward Lemurb9830242019-10-30 22:19:20 +00001702 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001703
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001704 if committing:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001705 sys.stdout.write('Running presubmit commit checks ...\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001706 else:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001707 sys.stdout.write('Running presubmit upload checks ...\n')
Edward Lemurecc27072020-01-06 16:42:34 +00001708 start_time = time_time()
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001709 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001710 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001711 if not presubmit_files and verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001712 sys.stdout.write('Warning, no PRESUBMIT.py found.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001713 results = []
Edward Lesmes8e282792018-04-03 18:50:29 -04001714 thread_pool = ThreadPool()
Edward Lemur7e3c67f2018-07-20 20:52:49 +00001715 executer = PresubmitExecuter(change, committing, verbose, gerrit_obj,
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001716 dry_run, thread_pool, parallel)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001717 if default_presubmit:
1718 if verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001719 sys.stdout.write('Running default presubmit script.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001720 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1721 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1722 for filename in presubmit_files:
1723 filename = os.path.abspath(filename)
1724 if verbose:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001725 sys.stdout.write('Running %s\n' % filename)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001726 # Accept CRLF presubmit script.
1727 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1728 results += executer.ExecPresubmitScript(presubmit_script, filename)
Edward Lesmes8e282792018-04-03 18:50:29 -04001729 results += thread_pool.RunAsync()
1730
Edward Lemur6eb1d322020-02-27 22:20:15 +00001731 messages = {}
1732 should_prompt = False
1733 presubmits_failed = False
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001734 for result in results:
1735 if result.fatal:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001736 presubmits_failed = True
1737 messages.setdefault('ERRORS', []).append(result)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001738 elif result.should_prompt:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001739 should_prompt = True
1740 messages.setdefault('Warnings', []).append(result)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001741 else:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001742 messages.setdefault('Messages', []).append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001743
Edward Lemur6eb1d322020-02-27 22:20:15 +00001744 sys.stdout.write('\n')
1745 for name, items in messages.items():
1746 sys.stdout.write('** Presubmit %s **\n' % name)
1747 for item in items:
1748 item.handle()
1749 sys.stdout.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001750
Edward Lemurecc27072020-01-06 16:42:34 +00001751 total_time = time_time() - start_time
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001752 if total_time > 1.0:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001753 sys.stdout.write(
1754 'Presubmit checks took %.1fs to calculate.\n\n' % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001755
Edward Lemur6eb1d322020-02-27 22:20:15 +00001756 if not should_prompt and not presubmits_failed:
1757 sys.stdout.write('Presubmit checks passed.\n')
1758 elif should_prompt:
1759 sys.stdout.write('There were presubmit warnings. ')
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001760 if may_prompt:
Edward Lemur6eb1d322020-02-27 22:20:15 +00001761 presubmits_failed = not prompt_should_continue(
1762 'Are you sure you wish to continue? (y/N): ')
Garrett Beatyacb807e2020-08-28 18:17:24 +00001763 else:
1764 sys.stdout.write('\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001765
Edward Lemur1dc66e12020-02-21 21:36:34 +00001766 if json_output:
1767 # Write the presubmit results to json output
1768 presubmit_results = {
1769 'errors': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001770 error.json_format()
1771 for error in messages.get('ERRORS', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001772 ],
1773 'notifications': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001774 notification.json_format()
1775 for notification in messages.get('Messages', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001776 ],
1777 'warnings': [
Edward Lemur6eb1d322020-02-27 22:20:15 +00001778 warning.json_format()
1779 for warning in messages.get('Warnings', [])
Edward Lemur1dc66e12020-02-21 21:36:34 +00001780 ],
Edward Lemur6eb1d322020-02-27 22:20:15 +00001781 'more_cc': executer.more_cc,
Edward Lemur1dc66e12020-02-21 21:36:34 +00001782 }
1783
1784 gclient_utils.FileWrite(
1785 json_output, json.dumps(presubmit_results, sort_keys=True))
1786
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001787 global _ASKED_FOR_FEEDBACK
1788 # Ask for feedback one time out of 5.
1789 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
Edward Lemur6eb1d322020-02-27 22:20:15 +00001790 sys.stdout.write(
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001791 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1792 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1793 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001794 _ASKED_FOR_FEEDBACK = True
Edward Lemur6eb1d322020-02-27 22:20:15 +00001795
1796 return 1 if presubmits_failed else 0
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001797 finally:
1798 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001799
1800
Edward Lemur50984a62020-02-06 18:10:18 +00001801def _scan_sub_dirs(mask, recursive):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001802 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001803 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001804
1805 results = []
1806 for root, dirs, files in os.walk('.'):
1807 if '.svn' in dirs:
1808 dirs.remove('.svn')
1809 if '.git' in dirs:
1810 dirs.remove('.git')
1811 for name in files:
1812 if fnmatch.fnmatch(name, mask):
1813 results.append(os.path.join(root, name))
1814 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001815
1816
Edward Lemur50984a62020-02-06 18:10:18 +00001817def _parse_files(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001818 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001819 files = []
1820 for arg in args:
Edward Lemur50984a62020-02-06 18:10:18 +00001821 files.extend([('M', f) for f in _scan_sub_dirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001822 return files
1823
1824
Edward Lemur50984a62020-02-06 18:10:18 +00001825def _parse_change(parser, options):
1826 """Process change options.
1827
1828 Args:
1829 parser: The parser used to parse the arguments from command line.
1830 options: The arguments parsed from command line.
1831 Returns:
1832 A GitChange if the change root is a git repository, or a Change otherwise.
1833 """
1834 if options.files and options.all_files:
1835 parser.error('<files> cannot be specified when --all-files is set.')
1836
Edward Lesmes44ea3ff2020-02-05 00:14:30 +00001837 change_scm = scm.determine_scm(options.root)
Edward Lemur50984a62020-02-06 18:10:18 +00001838 if change_scm != 'git' and not options.files:
1839 parser.error('<files> is not optional for unversioned directories.')
1840
1841 if options.files:
1842 change_files = _parse_files(options.files, options.recursive)
1843 elif options.all_files:
1844 change_files = [('M', f) for f in scm.GIT.GetAllFiles(options.root)]
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001845 else:
Edward Lemur50984a62020-02-06 18:10:18 +00001846 change_files = scm.GIT.CaptureStatus(
Edward Lemur7f6dec02020-02-06 20:23:58 +00001847 options.root, options.upstream or None)
Edward Lemur50984a62020-02-06 18:10:18 +00001848
1849 logging.info('Found %d file(s).', len(change_files))
1850
1851 change_class = GitChange if change_scm == 'git' else Change
1852 return change_class(
1853 options.name,
1854 options.description,
1855 options.root,
1856 change_files,
1857 options.issue,
1858 options.patchset,
1859 options.author,
1860 upstream=options.upstream)
1861
1862
1863def _parse_gerrit_options(parser, options):
1864 """Process gerrit options.
1865
1866 SIDE EFFECTS: Modifies options.author and options.description from Gerrit if
1867 options.gerrit_fetch is set.
1868
1869 Args:
1870 parser: The parser used to parse the arguments from command line.
1871 options: The arguments parsed from command line.
1872 Returns:
Stephen Martinisfb09de22021-02-25 03:41:13 +00001873 A GerritAccessor object if options.gerrit_url is set, or None otherwise.
Edward Lemur50984a62020-02-06 18:10:18 +00001874 """
1875 gerrit_obj = None
Stephen Martinisfb09de22021-02-25 03:41:13 +00001876 if options.gerrit_url:
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001877 gerrit_obj = GerritAccessor(
1878 url=options.gerrit_url,
1879 project=options.gerrit_project,
1880 branch=options.gerrit_branch)
Edward Lemur50984a62020-02-06 18:10:18 +00001881
1882 if not options.gerrit_fetch:
1883 return gerrit_obj
1884
1885 if not options.gerrit_url or not options.issue or not options.patchset:
1886 parser.error(
1887 '--gerrit_fetch requires --gerrit_url, --issue and --patchset.')
1888
1889 options.author = gerrit_obj.GetChangeOwner(options.issue)
1890 options.description = gerrit_obj.GetChangeDescription(
1891 options.issue, options.patchset)
1892
1893 logging.info('Got author: "%s"', options.author)
1894 logging.info('Got description: """\n%s\n"""', options.description)
1895
1896 return gerrit_obj
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001897
1898
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001899@contextlib.contextmanager
1900def canned_check_filter(method_names):
1901 filtered = {}
1902 try:
1903 for method_name in method_names:
1904 if not hasattr(presubmit_canned_checks, method_name):
Gavin Make6a62332020-12-04 21:57:10 +00001905 logging.warning('Skipping unknown "canned" check %s' % method_name)
Aaron Gableecee74c2018-04-02 15:13:08 -07001906 continue
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001907 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1908 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1909 yield
1910 finally:
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001911 for name, method in filtered.items():
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001912 setattr(presubmit_canned_checks, name, method)
1913
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001914
sbc@chromium.org013731e2015-02-26 18:28:43 +00001915def main(argv=None):
Edward Lemura5799e32020-01-17 19:26:51 +00001916 parser = argparse.ArgumentParser(usage='%(prog)s [options] <files...>')
1917 hooks = parser.add_mutually_exclusive_group()
1918 hooks.add_argument('-c', '--commit', action='store_true',
1919 help='Use commit instead of upload checks.')
1920 hooks.add_argument('-u', '--upload', action='store_false', dest='commit',
1921 help='Use upload instead of commit checks.')
Edward Lemur75526302020-02-27 22:31:05 +00001922 hooks.add_argument('--post_upload', action='store_true',
1923 help='Run post-upload commit hooks.')
Edward Lemura5799e32020-01-17 19:26:51 +00001924 parser.add_argument('-r', '--recursive', action='store_true',
1925 help='Act recursively.')
1926 parser.add_argument('-v', '--verbose', action='count', default=0,
1927 help='Use 2 times for more debug info.')
1928 parser.add_argument('--name', default='no name')
1929 parser.add_argument('--author')
Edward Lemur227d5102020-02-25 23:45:35 +00001930 desc = parser.add_mutually_exclusive_group()
1931 desc.add_argument('--description', default='', help='The change description.')
1932 desc.add_argument('--description_file',
1933 help='File to read change description from.')
Edward Lemura5799e32020-01-17 19:26:51 +00001934 parser.add_argument('--issue', type=int, default=0)
1935 parser.add_argument('--patchset', type=int, default=0)
1936 parser.add_argument('--root', default=os.getcwd(),
1937 help='Search for PRESUBMIT.py up to this directory. '
1938 'If inherit-review-settings-ok is present in this '
1939 'directory, parent directories up to the root file '
1940 'system directories will also be searched.')
1941 parser.add_argument('--upstream',
1942 help='Git only: the base ref or upstream branch against '
1943 'which the diff should be computed.')
1944 parser.add_argument('--default_presubmit')
1945 parser.add_argument('--may_prompt', action='store_true', default=False)
1946 parser.add_argument('--skip_canned', action='append', default=[],
1947 help='A list of checks to skip which appear in '
1948 'presubmit_canned_checks. Can be provided multiple times '
1949 'to skip multiple canned checks.')
1950 parser.add_argument('--dry_run', action='store_true', help=argparse.SUPPRESS)
1951 parser.add_argument('--gerrit_url', help=argparse.SUPPRESS)
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001952 parser.add_argument('--gerrit_project', help=argparse.SUPPRESS)
1953 parser.add_argument('--gerrit_branch', help=argparse.SUPPRESS)
Edward Lemura5799e32020-01-17 19:26:51 +00001954 parser.add_argument('--gerrit_fetch', action='store_true',
1955 help=argparse.SUPPRESS)
1956 parser.add_argument('--parallel', action='store_true',
1957 help='Run all tests specified by input_api.RunTests in '
1958 'all PRESUBMIT files in parallel.')
1959 parser.add_argument('--json_output',
1960 help='Write presubmit errors to json output.')
Edward Lemur1dc66e12020-02-21 21:36:34 +00001961 parser.add_argument('--all_files', action='store_true',
Edward Lemur50984a62020-02-06 18:10:18 +00001962 help='Mark all files under source control as modified.')
Edward Lemura5799e32020-01-17 19:26:51 +00001963 parser.add_argument('files', nargs='*',
1964 help='List of files to be marked as modified when '
1965 'executing presubmit or post-upload hooks. fnmatch '
1966 'wildcards can also be used.')
Edward Lemura5799e32020-01-17 19:26:51 +00001967 options = parser.parse_args(argv)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001968
Erik Staabcca5c492020-04-16 17:40:07 +00001969 log_level = logging.ERROR
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001970 if options.verbose >= 2:
Erik Staabcca5c492020-04-16 17:40:07 +00001971 log_level = logging.DEBUG
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001972 elif options.verbose:
Erik Staabcca5c492020-04-16 17:40:07 +00001973 log_level = logging.INFO
1974 log_format = ('[%(levelname).1s%(asctime)s %(process)d %(thread)d '
1975 '%(filename)s] %(message)s')
1976 logging.basicConfig(format=log_format, level=log_level)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001977
Edward Lemur227d5102020-02-25 23:45:35 +00001978 if options.description_file:
1979 options.description = gclient_utils.FileRead(options.description_file)
Edward Lemur50984a62020-02-06 18:10:18 +00001980 gerrit_obj = _parse_gerrit_options(parser, options)
1981 change = _parse_change(parser, options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001982
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001983 try:
Edward Lemur75526302020-02-27 22:31:05 +00001984 if options.post_upload:
1985 return DoPostUploadExecuter(
1986 change,
1987 gerrit_obj,
1988 options.verbose)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001989 with canned_check_filter(options.skip_canned):
Edward Lemur6eb1d322020-02-27 22:20:15 +00001990 return DoPresubmitChecks(
Edward Lemur50984a62020-02-06 18:10:18 +00001991 change,
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001992 options.commit,
1993 options.verbose,
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001994 options.default_presubmit,
1995 options.may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001996 gerrit_obj,
Edward Lesmes8e282792018-04-03 18:50:29 -04001997 options.dry_run,
Debrian Figueroadd2737e2019-06-21 23:50:13 +00001998 options.parallel,
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001999 options.json_output)
Raul Tambre7c938462019-05-24 16:35:35 +00002000 except PresubmitFailure as e:
Raul Tambre80ee78e2019-05-06 22:41:05 +00002001 print(e, file=sys.stderr)
2002 print('Maybe your depot_tools is out of date?', file=sys.stderr)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002003 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00002004
2005
2006if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00002007 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00002008 try:
2009 sys.exit(main())
2010 except KeyboardInterrupt:
2011 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07002012 sys.exit(2)