blob: 89f7d5ad3dd342988c340195360ddebfffd5ec9b [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
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00009__version__ = '1.8.0'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000010
11# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
12# caching (between all different invocations of presubmit scripts for a given
13# change). We should add it as our presubmit scripts start feeling slow.
14
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +090015import ast # Exposed through the API.
enne@chromium.orge72c5f52013-04-16 00:36:40 +000016import cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000017import cPickle # Exposed through the API.
18import cStringIO # Exposed through the API.
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +000019import contextlib
dcheng091b7db2016-06-16 01:27:51 -070020import fnmatch # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000021import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000022import inspect
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +000023import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000024import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000025import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import marshal # Exposed through the API.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000027import multiprocessing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000028import optparse
29import os # Somewhat exposed through the API.
30import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000031import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import re # Exposed through the API.
Edward Lesmesc7d0b342018-03-28 21:19:00 -040033import signal
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034import sys # Parts exposed through API.
35import tempfile # Exposed through the API.
Edward Lesmesc7d0b342018-03-28 21:19:00 -040036import threading
jam@chromium.org2a891dc2009-08-20 20:33:37 +000037import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000038import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000039import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000040import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000041import urllib2 # Exposed through the API.
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000042import urlparse
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000043from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000044
45# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000046import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000047import gclient_utils
Aaron Gableb584c4f2017-04-26 16:28:08 -070048import git_footers
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000049import gerrit_util
dpranke@chromium.org2a009622011-03-01 02:43:31 +000050import owners
Jochen Eisinger76f5fc62017-04-07 16:27:46 +020051import owners_finder
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052import presubmit_canned_checks
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000053import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000054import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000055
56
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000057# Ask for feedback only once in program lifetime.
58_ASKED_FOR_FEEDBACK = False
59
60
maruel@chromium.org899e1c12011-04-07 17:03:18 +000061class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000062 pass
63
64
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000065class CommandData(object):
66 def __init__(self, name, cmd, kwargs, message):
67 self.name = name
68 self.cmd = cmd
Edward Lesmesc7d0b342018-03-28 21:19:00 -040069 self.stdin = kwargs.get('stdin', None)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000070 self.kwargs = kwargs
Edward Lesmesc7d0b342018-03-28 21:19:00 -040071 self.kwargs['stdout'] = subprocess.PIPE
72 self.kwargs['stderr'] = subprocess.STDOUT
73 self.kwargs['stdin'] = subprocess.PIPE
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000074 self.message = message
75 self.info = None
76
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000077
Edward Lesmesc7d0b342018-03-28 21:19:00 -040078# Adapted from
79# https://github.com/google/gtest-parallel/blob/master/gtest_parallel.py#L37
80#
81# An object that catches SIGINT sent to the Python process and notices
82# if processes passed to wait() die by SIGINT (we need to look for
83# both of those cases, because pressing Ctrl+C can result in either
84# the main process or one of the subprocesses getting the signal).
85#
86# Before a SIGINT is seen, wait(p) will simply call p.wait() and
87# return the result. Once a SIGINT has been seen (in the main process
88# or a subprocess, including the one the current call is waiting for),
89# wait(p) will call p.terminate() and raise ProcessWasInterrupted.
90class SigintHandler(object):
91 class ProcessWasInterrupted(Exception):
92 pass
93
94 sigint_returncodes = {-signal.SIGINT, # Unix
95 -1073741510, # Windows
96 }
97 def __init__(self):
98 self.__lock = threading.Lock()
99 self.__processes = set()
100 self.__got_sigint = False
101 signal.signal(signal.SIGINT, lambda signal_num, frame: self.interrupt())
102
103 def __on_sigint(self):
104 self.__got_sigint = True
105 while self.__processes:
106 try:
107 self.__processes.pop().terminate()
108 except OSError:
109 pass
110
111 def interrupt(self):
112 with self.__lock:
113 self.__on_sigint()
114
115 def got_sigint(self):
116 with self.__lock:
117 return self.__got_sigint
118
119 def wait(self, p, stdin):
120 with self.__lock:
121 if self.__got_sigint:
122 p.terminate()
123 self.__processes.add(p)
124 stdout, stderr = p.communicate(stdin)
125 code = p.returncode
126 with self.__lock:
127 self.__processes.discard(p)
128 if code in self.sigint_returncodes:
129 self.__on_sigint()
130 if self.__got_sigint:
131 raise self.ProcessWasInterrupted
132 return stdout, stderr
133
134sigint_handler = SigintHandler()
135
136
137class ThreadPool(object):
138 def __init__(self, pool_size=None):
139 self._tests = []
140 self._nonparallel_tests = []
141 self._pool_size = pool_size or multiprocessing.cpu_count()
142 self._messages = []
143 self._messages_lock = threading.Lock()
144 self._current_index = 0
145 self._current_index_lock = threading.Lock()
146
147 def CallCommand(self, test):
148 """Runs an external program.
149
150 This function converts invocation of .py files and invocations of "python"
151 to vpython invocations.
152 """
153 vpython = 'vpython.bat' if sys.platform == 'win32' else 'vpython'
154
155 cmd = test.cmd
156 if cmd[0] == 'python':
157 cmd = list(cmd)
158 cmd[0] = vpython
159 elif cmd[0].endswith('.py'):
160 cmd = [vpython] + cmd
161
162 try:
163 start = time.time()
164 p = subprocess.Popen(cmd, **test.kwargs)
165 stdout, _ = sigint_handler.wait(p, test.stdin)
166 duration = time.time() - start
167 except OSError as e:
168 duration = time.time() - start
169 return test.message(
170 '%s exec failure (%4.2fs)\n %s' % (test.name, duration, e))
171 if p.returncode != 0:
172 return test.message(
173 '%s (%4.2fs) failed\n%s' % (test.name, duration, stdout))
174 if test.info:
175 return test.info('%s (%4.2fs)' % (test.name, duration))
176
177 def AddTests(self, tests, parallel=True):
178 if parallel:
179 self._tests.extend(tests)
180 else:
181 self._nonparallel_tests.extend(tests)
182
183 def RunAsync(self):
184 def _WorkerFn():
185 while True:
186 test_index = None
187 with self._current_index_lock:
188 if self._current_index == len(self._tests):
189 break
190 test_index = self._current_index
191 self._current_index += 1
192 result = self.CallCommand(self._tests[test_index])
193 if result:
194 with self._messages_lock:
195 self._messages.append(result)
196
197 def _StartDaemon():
198 t = threading.Thread(target=_WorkerFn)
199 t.daemon = True
200 t.start()
201 return t
202
203 for test in self._nonparallel_tests:
204 result = self.CallCommand(test)
205 if result:
206 self._messages.append(result)
207
208 if self._tests:
209 threads = [_StartDaemon() for _ in range(self._pool_size)]
210 for worker in threads:
211 worker.join()
212
213 return self._messages
214
215
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000216def normpath(path):
217 '''Version of os.path.normpath that also changes backward slashes to
218 forward slashes when not running on Windows.
219 '''
220 # This is safe to always do because the Windows version of os.path.normpath
221 # will replace forward slashes with backward slashes.
222 path = path.replace(os.sep, '/')
223 return os.path.normpath(path)
224
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000225
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000226def _RightHandSideLinesImpl(affected_files):
227 """Implements RightHandSideLines for InputApi and GclChange."""
228 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000229 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000230 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000231 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000232
233
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000234class PresubmitOutput(object):
235 def __init__(self, input_stream=None, output_stream=None):
236 self.input_stream = input_stream
237 self.output_stream = output_stream
238 self.reviewers = []
Daniel Cheng7227d212017-11-17 08:12:37 -0800239 self.more_cc = []
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000240 self.written_output = []
241 self.error_count = 0
242
243 def prompt_yes_no(self, prompt_string):
244 self.write(prompt_string)
245 if self.input_stream:
246 response = self.input_stream.readline().strip().lower()
247 if response not in ('y', 'yes'):
248 self.fail()
249 else:
250 self.fail()
251
252 def fail(self):
253 self.error_count += 1
254
255 def should_continue(self):
256 return not self.error_count
257
258 def write(self, s):
259 self.written_output.append(s)
260 if self.output_stream:
261 self.output_stream.write(s)
262
263 def getvalue(self):
264 return ''.join(self.written_output)
265
266
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000267# Top level object so multiprocessing can pickle
268# Public access through OutputApi object.
269class _PresubmitResult(object):
270 """Base class for result objects."""
271 fatal = False
272 should_prompt = False
273
274 def __init__(self, message, items=None, long_text=''):
275 """
276 message: A short one-line message to indicate errors.
277 items: A list of short strings to indicate where errors occurred.
278 long_text: multi-line text output, e.g. from another tool
279 """
280 self._message = message
281 self._items = items or []
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000282 self._long_text = long_text.rstrip()
283
284 def handle(self, output):
285 output.write(self._message)
286 output.write('\n')
287 for index, item in enumerate(self._items):
288 output.write(' ')
289 # Write separately in case it's unicode.
290 output.write(str(item))
291 if index < len(self._items) - 1:
292 output.write(' \\')
293 output.write('\n')
294 if self._long_text:
295 output.write('\n***************\n')
296 # Write separately in case it's unicode.
297 output.write(self._long_text)
298 output.write('\n***************\n')
299 if self.fatal:
300 output.fail()
301
302
303# Top level object so multiprocessing can pickle
304# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000305class _PresubmitError(_PresubmitResult):
306 """A hard presubmit error."""
307 fatal = True
308
309
310# Top level object so multiprocessing can pickle
311# Public access through OutputApi object.
312class _PresubmitPromptWarning(_PresubmitResult):
313 """An warning that prompts the user if they want to continue."""
314 should_prompt = True
315
316
317# Top level object so multiprocessing can pickle
318# Public access through OutputApi object.
319class _PresubmitNotifyResult(_PresubmitResult):
320 """Just print something to the screen -- but it's not even a warning."""
321 pass
322
323
324# Top level object so multiprocessing can pickle
325# Public access through OutputApi object.
326class _MailTextResult(_PresubmitResult):
327 """A warning that should be included in the review request email."""
328 def __init__(self, *args, **kwargs):
329 super(_MailTextResult, self).__init__()
330 raise NotImplementedError()
331
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000332class GerritAccessor(object):
333 """Limited Gerrit functionality for canned presubmit checks to work.
334
335 To avoid excessive Gerrit calls, caches the results.
336 """
337
338 def __init__(self, host):
339 self.host = host
340 self.cache = {}
341
342 def _FetchChangeDetail(self, issue):
343 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100344 try:
345 return gerrit_util.GetChangeDetail(
346 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700347 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100348 except gerrit_util.GerritError as e:
349 if e.http_status == 404:
350 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
351 'no credentials to fetch issue details' % issue)
352 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000353
354 def GetChangeInfo(self, issue):
355 """Returns labels and all revisions (patchsets) for this issue.
356
357 The result is a dictionary according to Gerrit REST Api.
358 https://gerrit-review.googlesource.com/Documentation/rest-api.html
359
360 However, API isn't very clear what's inside, so see tests for example.
361 """
362 assert issue
363 cache_key = int(issue)
364 if cache_key not in self.cache:
365 self.cache[cache_key] = self._FetchChangeDetail(issue)
366 return self.cache[cache_key]
367
368 def GetChangeDescription(self, issue, patchset=None):
369 """If patchset is none, fetches current patchset."""
370 info = self.GetChangeInfo(issue)
371 # info is a reference to cache. We'll modify it here adding description to
372 # it to the right patchset, if it is not yet there.
373
374 # Find revision info for the patchset we want.
375 if patchset is not None:
376 for rev, rev_info in info['revisions'].iteritems():
377 if str(rev_info['_number']) == str(patchset):
378 break
379 else:
380 raise Exception('patchset %s doesn\'t exist in issue %s' % (
381 patchset, issue))
382 else:
383 rev = info['current_revision']
384 rev_info = info['revisions'][rev]
385
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100386 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000387
Mun Yong Jang603d01e2017-12-19 16:38:30 -0800388 def GetDestRef(self, issue):
389 ref = self.GetChangeInfo(issue)['branch']
390 if not ref.startswith('refs/'):
391 # NOTE: it is possible to create 'refs/x' branch,
392 # aka 'refs/heads/refs/x'. However, this is ill-advised.
393 ref = 'refs/heads/%s' % ref
394 return ref
395
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000396 def GetChangeOwner(self, issue):
397 return self.GetChangeInfo(issue)['owner']['email']
398
399 def GetChangeReviewers(self, issue, approving_only=True):
Aaron Gable8b478f02017-07-31 15:33:19 -0700400 changeinfo = self.GetChangeInfo(issue)
401 if approving_only:
402 labelinfo = changeinfo.get('labels', {}).get('Code-Review', {})
403 values = labelinfo.get('values', {}).keys()
404 try:
405 max_value = max(int(v) for v in values)
406 reviewers = [r for r in labelinfo.get('all', [])
407 if r.get('value', 0) == max_value]
408 except ValueError: # values is the empty list
409 reviewers = []
410 else:
411 reviewers = changeinfo.get('reviewers', {}).get('REVIEWER', [])
412 return [r.get('email') for r in reviewers]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000413
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000414
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000415class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000416 """An instance of OutputApi gets passed to presubmit scripts so that they
417 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000418 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000419 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000420 PresubmitError = _PresubmitError
421 PresubmitPromptWarning = _PresubmitPromptWarning
422 PresubmitNotifyResult = _PresubmitNotifyResult
423 MailTextResult = _MailTextResult
424
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000425 def __init__(self, is_committing):
426 self.is_committing = is_committing
Daniel Cheng7227d212017-11-17 08:12:37 -0800427 self.more_cc = []
428
429 def AppendCC(self, cc):
430 """Appends a user to cc for this change."""
431 self.more_cc.append(cc)
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000432
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000433 def PresubmitPromptOrNotify(self, *args, **kwargs):
434 """Warn the user when uploading, but only notify if committing."""
435 if self.is_committing:
436 return self.PresubmitNotifyResult(*args, **kwargs)
437 return self.PresubmitPromptWarning(*args, **kwargs)
438
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800439 def EnsureCQIncludeTrybotsAreAdded(self, cl, bots_to_include, message):
440 """Helper for any PostUploadHook wishing to add CQ_INCLUDE_TRYBOTS.
441
442 Merges the bots_to_include into the current CQ_INCLUDE_TRYBOTS list,
443 keeping it alphabetically sorted. Returns the results that should be
444 returned from the PostUploadHook.
445
446 Args:
447 cl: The git_cl.Changelist object.
448 bots_to_include: A list of strings of bots to include, in the form
449 "master:slave".
450 message: A message to be printed in the case that
451 CQ_INCLUDE_TRYBOTS was updated.
452 """
453 description = cl.GetDescription(force=True)
Aaron Gableb584c4f2017-04-26 16:28:08 -0700454 include_re = re.compile(r'^CQ_INCLUDE_TRYBOTS=(.*)$', re.M | re.I)
455
456 prior_bots = []
457 if cl.IsGerrit():
458 trybot_footers = git_footers.parse_footers(description).get(
459 git_footers.normalize_name('Cq-Include-Trybots'), [])
460 for f in trybot_footers:
461 prior_bots += [b.strip() for b in f.split(';') if b.strip()]
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800462 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700463 trybot_tags = include_re.finditer(description)
464 for t in trybot_tags:
465 prior_bots += [b.strip() for b in t.group(1).split(';') if b.strip()]
466
467 if set(prior_bots) >= set(bots_to_include):
468 return []
469 all_bots = ';'.join(sorted(set(prior_bots) | set(bots_to_include)))
470
471 if cl.IsGerrit():
472 description = git_footers.remove_footer(
473 description, 'Cq-Include-Trybots')
474 description = git_footers.add_footer(
475 description, 'Cq-Include-Trybots', all_bots,
476 before_keys=['Change-Id'])
477 else:
478 new_include_trybots = 'CQ_INCLUDE_TRYBOTS=%s' % all_bots
479 m = include_re.search(description)
480 if m:
481 description = include_re.sub(new_include_trybots, description)
Kenneth Russelldf6e7342017-04-24 17:07:41 -0700482 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700483 description = '%s\n%s\n' % (description, new_include_trybots)
484
485 cl.UpdateDescription(description, force=True)
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800486 return [self.PresubmitNotifyResult(message)]
487
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000488
489class InputApi(object):
490 """An instance of this object is passed to presubmit scripts so they can
491 know stuff about the change they're looking at.
492 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000493 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800494 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000495
maruel@chromium.org3410d912009-06-09 20:56:16 +0000496 # File extensions that are considered source files from a style guide
497 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000498 #
499 # Files without an extension aren't included in the list. If you want to
500 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
501 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000502 DEFAULT_WHITE_LIST = (
503 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000504 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
505 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000506 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000507 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000508 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000509 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000510 )
511
512 # Path regexp that should be excluded from being considered containing source
513 # files. Don't modify this list from a presubmit script!
514 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000515 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000516 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000517 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
518 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000519 # Output directories (just in case)
520 r".*\bDebug[\\\/].*",
521 r".*\bRelease[\\\/].*",
522 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000523 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000524 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000525 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000526 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000527 r"(|.*[\\\/])\.git[\\\/].*",
528 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000529 # There is no point in processing a patch file.
530 r".+\.diff$",
531 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000532 )
533
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000534 def __init__(self, change, presubmit_path, is_committing,
Aaron Gable668c1d82018-04-03 10:19:16 -0700535 verbose, gerrit_obj, dry_run=None, thread_pool=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000536 """Builds an InputApi object.
537
538 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000539 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000540 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000541 is_committing: True if the change is about to be committed.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000542 gerrit_obj: provides basic Gerrit codereview functionality.
543 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000544 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000545 # Version number of the presubmit_support script.
546 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000547 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000548 self.is_committing = is_committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000549 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000550 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000551
Edward Lesmesc7d0b342018-03-28 21:19:00 -0400552 self.thread_pool = thread_pool or ThreadPool()
553
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000554 # We expose various modules and functions as attributes of the input_api
555 # so that presubmit scripts don't have to import them.
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +0900556 self.ast = ast
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000557 self.basename = os.path.basename
558 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000559 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000560 self.cStringIO = cStringIO
dcheng091b7db2016-06-16 01:27:51 -0700561 self.fnmatch = fnmatch
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000562 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000563 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000564 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000565 self.os_listdir = os.listdir
566 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000567 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000568 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000569 self.pickle = pickle
570 self.marshal = marshal
571 self.re = re
572 self.subprocess = subprocess
573 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000574 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000575 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000576 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000577 self.urllib2 = urllib2
578
Robert Iannucci50258932018-03-19 10:30:59 -0700579 self.is_windows = sys.platform == 'win32'
580
581 # Set python_executable to 'python'. This is interpreted in CallCommand to
582 # convert to vpython in order to allow scripts in other repos (e.g. src.git)
583 # to automatically pick up that repo's .vpython file, instead of inheriting
584 # the one in depot_tools.
585 self.python_executable = 'python'
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000586 self.environ = os.environ
587
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000588 # InputApi.platform is the platform you're currently running on.
589 self.platform = sys.platform
590
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000591 self.cpu_count = multiprocessing.cpu_count()
592
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000593 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000594 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000595
596 # We carry the canned checks so presubmit scripts can easily use them.
597 self.canned_checks = presubmit_canned_checks
598
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100599 # Temporary files we must manually remove at the end of a run.
600 self._named_temporary_files = []
Jochen Eisinger72606f82017-04-04 10:44:18 +0200601
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000602 # TODO(dpranke): figure out a list of all approved owners for a repo
603 # in order to be able to handle wildcard OWNERS files?
604 self.owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +0200605 fopen=file, os_path=self.os_path)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200606 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000607 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000608 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000609
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000610 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000611 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000612 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800613 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000614 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000615 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000616 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
617 for (a, b, header) in cpplint._re_pattern_templates
618 ]
619
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000620 def PresubmitLocalPath(self):
621 """Returns the local path of the presubmit script currently being run.
622
623 This is useful if you don't want to hard-code absolute paths in the
624 presubmit script. For example, It can be used to find another file
625 relative to the PRESUBMIT.py script, so the whole tree can be branched and
626 the presubmit script still works, without editing its content.
627 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000628 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000629
agable0b65e732016-11-22 09:25:46 -0800630 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000631 """Same as input_api.change.AffectedFiles() except only lists files
632 (and optionally directories) in the same directory as the current presubmit
633 script, or subdirectories thereof.
634 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000635 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000636 if len(dir_with_slash) == 1:
637 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000638
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000639 return filter(
640 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
agable0b65e732016-11-22 09:25:46 -0800641 self.change.AffectedFiles(include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000642
agable0b65e732016-11-22 09:25:46 -0800643 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000644 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800645 paths = [af.LocalPath() for af in self.AffectedFiles()]
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000646 logging.debug("LocalPaths: %s", paths)
647 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000648
agable0b65e732016-11-22 09:25:46 -0800649 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000650 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800651 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000652
agable0b65e732016-11-22 09:25:46 -0800653 def AffectedTestableFiles(self, include_deletes=None):
654 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000655 in the same directory as the current presubmit script, or subdirectories
656 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000657 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000658 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800659 warn("AffectedTestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000660 " is deprecated and ignored" % str(include_deletes),
661 category=DeprecationWarning,
662 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800663 return filter(lambda x: x.IsTestableFile(),
664 self.AffectedFiles(include_deletes=False))
665
666 def AffectedTextFiles(self, include_deletes=None):
667 """An alias to AffectedTestableFiles for backwards compatibility."""
668 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000669
maruel@chromium.org3410d912009-06-09 20:56:16 +0000670 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
671 """Filters out files that aren't considered "source file".
672
673 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
674 and InputApi.DEFAULT_BLACK_LIST is used respectively.
675
676 The lists will be compiled as regular expression and
677 AffectedFile.LocalPath() needs to pass both list.
678
679 Note: Copy-paste this function to suit your needs or use a lambda function.
680 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000681 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000682 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000683 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000684 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000685 return True
686 return False
687 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
688 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
689
690 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800691 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000692
693 If source_file is None, InputApi.FilterSourceFile() is used.
694 """
695 if not source_file:
696 source_file = self.FilterSourceFile
agable0b65e732016-11-22 09:25:46 -0800697 return filter(source_file, self.AffectedTestableFiles())
maruel@chromium.org3410d912009-06-09 20:56:16 +0000698
699 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000700 """An iterator over all text lines in "new" version of changed files.
701
702 Only lists lines from new or modified text files in the change that are
703 contained by the directory of the currently executing presubmit script.
704
705 This is useful for doing line-by-line regex checks, like checking for
706 trailing whitespace.
707
708 Yields:
709 a 3 tuple:
710 the AffectedFile instance of the current file;
711 integer line number (1-based); and
712 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000713
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000714 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000715 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000716 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000717 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000718
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000719 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000720 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000721
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000722 Deny reading anything outside the repository.
723 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000724 if isinstance(file_item, AffectedFile):
725 file_item = file_item.AbsoluteLocalPath()
726 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000727 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000728 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000729
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100730 def CreateTemporaryFile(self, **kwargs):
731 """Returns a named temporary file that must be removed with a call to
732 RemoveTemporaryFiles().
733
734 All keyword arguments are forwarded to tempfile.NamedTemporaryFile(),
735 except for |delete|, which is always set to False.
736
737 Presubmit checks that need to create a temporary file and pass it for
738 reading should use this function instead of NamedTemporaryFile(), as
739 Windows fails to open a file that is already open for writing.
740
741 with input_api.CreateTemporaryFile() as f:
742 f.write('xyz')
743 f.close()
744 input_api.subprocess.check_output(['script-that', '--reads-from',
745 f.name])
746
747
748 Note that callers of CreateTemporaryFile() should not worry about removing
749 any temporary file; this is done transparently by the presubmit handling
750 code.
751 """
752 if 'delete' in kwargs:
753 # Prevent users from passing |delete|; we take care of file deletion
754 # ourselves and this prevents unintuitive error messages when we pass
755 # delete=False and 'delete' is also in kwargs.
756 raise TypeError('CreateTemporaryFile() does not take a "delete" '
757 'argument, file deletion is handled automatically by '
758 'the same presubmit_support code that creates InputApi '
759 'objects.')
760 temp_file = self.tempfile.NamedTemporaryFile(delete=False, **kwargs)
761 self._named_temporary_files.append(temp_file.name)
762 return temp_file
763
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000764 @property
765 def tbr(self):
766 """Returns if a change is TBR'ed."""
Jeremy Romandce22502017-06-20 15:37:29 -0400767 return 'TBR' in self.change.tags or self.change.TBRsFromDescription()
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000768
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000769 def RunTests(self, tests_mix, parallel=True):
Edward Lesmesc7d0b342018-03-28 21:19:00 -0400770 # RunTests doesn't actually run tests. It adds them to a ThreadPool that
771 # will run all tests once all PRESUBMIT files are processed.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000772 tests = []
773 msgs = []
774 for t in tests_mix:
Edward Lesmesc7d0b342018-03-28 21:19:00 -0400775 if isinstance(t, OutputApi.PresubmitResult) and t:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000776 msgs.append(t)
777 else:
778 assert issubclass(t.message, _PresubmitResult)
779 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000780 if self.verbose:
781 t.info = _PresubmitNotifyResult
Edward Lesmesc7d0b342018-03-28 21:19:00 -0400782 t.kwargs['cwd'] = self.PresubmitLocalPath()
783 self.thread_pool.AddTests(tests, parallel)
784 return msgs
scottmg86099d72016-09-01 09:16:51 -0700785
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000786
nick@chromium.orgff526192013-06-10 19:30:26 +0000787class _DiffCache(object):
788 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000789 def __init__(self, upstream=None):
790 """Stores the upstream revision against which all diffs will be computed."""
791 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000792
793 def GetDiff(self, path, local_root):
794 """Get the diff for a particular path."""
795 raise NotImplementedError()
796
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700797 def GetOldContents(self, path, local_root):
798 """Get the old version for a particular path."""
799 raise NotImplementedError()
800
nick@chromium.orgff526192013-06-10 19:30:26 +0000801
nick@chromium.orgff526192013-06-10 19:30:26 +0000802class _GitDiffCache(_DiffCache):
803 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000804 def __init__(self, upstream):
805 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000806 self._diffs_by_file = None
807
808 def GetDiff(self, path, local_root):
809 if not self._diffs_by_file:
810 # Compute a single diff for all files and parse the output; should
811 # with git this is much faster than computing one diff for each file.
812 diffs = {}
813
814 # Don't specify any filenames below, because there are command line length
815 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000816 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
817 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000818
819 # This regex matches the path twice, separated by a space. Note that
820 # filename itself may contain spaces.
821 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
822 current_diff = []
823 keep_line_endings = True
824 for x in unified_diff.splitlines(keep_line_endings):
825 match = file_marker.match(x)
826 if match:
827 # Marks the start of a new per-file section.
828 diffs[match.group('filename')] = current_diff = [x]
829 elif x.startswith('diff --git'):
830 raise PresubmitFailure('Unexpected diff line: %s' % x)
831 else:
832 current_diff.append(x)
833
834 self._diffs_by_file = dict(
835 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
836
837 if path not in self._diffs_by_file:
838 raise PresubmitFailure(
839 'Unified diff did not contain entry for file %s' % path)
840
841 return self._diffs_by_file[path]
842
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700843 def GetOldContents(self, path, local_root):
844 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
845
nick@chromium.orgff526192013-06-10 19:30:26 +0000846
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000847class AffectedFile(object):
848 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000849
850 DIFF_CACHE = _DiffCache
851
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000852 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800853 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000854 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000855 self._path = path
856 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000857 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000858 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000859 self._cached_changed_contents = None
860 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000861 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700862 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000863
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000864 def LocalPath(self):
865 """Returns the path of this file on the local disk relative to client root.
Andrew Grieve92b8b992017-11-02 09:42:24 -0400866
867 This should be used for error messages but not for accessing files,
868 because presubmit checks are run with CWD=PresubmitLocalPath() (which is
869 often != client root).
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000870 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000871 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000872
873 def AbsoluteLocalPath(self):
874 """Returns the absolute path of this file on the local disk.
875 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000876 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000877
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000878 def Action(self):
879 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000880 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000881
agable0b65e732016-11-22 09:25:46 -0800882 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000883 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000884
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000885 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000886 raise NotImplementedError() # Implement when needed
887
agable0b65e732016-11-22 09:25:46 -0800888 def IsTextFile(self):
889 """An alias to IsTestableFile for backwards compatibility."""
890 return self.IsTestableFile()
891
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700892 def OldContents(self):
893 """Returns an iterator over the lines in the old version of file.
894
Daniel Cheng2da34fe2017-03-21 20:42:12 -0700895 The old version is the file before any modifications in the user's
896 workspace, i.e. the "left hand side".
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700897
898 Contents will be empty if the file is a directory or does not exist.
899 Note: The carriage returns (LF or CR) are stripped off.
900 """
901 return self._diff_cache.GetOldContents(self.LocalPath(),
902 self._local_root).splitlines()
903
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000904 def NewContents(self):
905 """Returns an iterator over the lines in the new version of file.
906
907 The new version is the file in the user's workspace, i.e. the "right hand
908 side".
909
910 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000911 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000912 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000913 if self._cached_new_contents is None:
914 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800915 try:
916 self._cached_new_contents = gclient_utils.FileRead(
917 self.AbsoluteLocalPath(), 'rU').splitlines()
918 except IOError:
919 pass # File not found? That's fine; maybe it was deleted.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000920 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000921
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000922 def ChangedContents(self):
923 """Returns a list of tuples (line number, line text) of all new lines.
924
925 This relies on the scm diff output describing each changed code section
926 with a line of the form
927
928 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
929 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000930 if self._cached_changed_contents is not None:
931 return self._cached_changed_contents[:]
932 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000933 line_num = 0
934
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000935 for line in self.GenerateScmDiff().splitlines():
936 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
937 if m:
938 line_num = int(m.groups(1)[0])
939 continue
940 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000941 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000942 if not line.startswith('-'):
943 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000944 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000945
maruel@chromium.org5de13972009-06-10 18:16:06 +0000946 def __str__(self):
947 return self.LocalPath()
948
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000949 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000950 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000951
maruel@chromium.org58407af2011-04-12 23:15:57 +0000952
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000953class GitAffectedFile(AffectedFile):
954 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000955 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800956 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000957
nick@chromium.orgff526192013-06-10 19:30:26 +0000958 DIFF_CACHE = _GitDiffCache
959
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000960 def __init__(self, *args, **kwargs):
961 AffectedFile.__init__(self, *args, **kwargs)
962 self._server_path = None
agable0b65e732016-11-22 09:25:46 -0800963 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000964
agable0b65e732016-11-22 09:25:46 -0800965 def IsTestableFile(self):
966 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000967 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -0800968 # A deleted file is not testable.
969 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000970 else:
agable0b65e732016-11-22 09:25:46 -0800971 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
972 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000973
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000974
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000975class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000976 """Describe a change.
977
978 Used directly by the presubmit scripts to query the current change being
979 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000980
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000981 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000982 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000983 self.KEY: equivalent to tags['KEY']
984 """
985
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000986 _AFFECTED_FILES = AffectedFile
987
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000988 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000989 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000990 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000991 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000992
maruel@chromium.org58407af2011-04-12 23:15:57 +0000993 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000994 self, name, description, local_root, files, issue, patchset, author,
995 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000996 if files is None:
997 files = []
998 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000999 # Convert root into an absolute path.
1000 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001001 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001002 self.issue = issue
1003 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +00001004 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001005
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001006 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001007 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001008 self._description_without_tags = ''
1009 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010
maruel@chromium.orge085d812011-10-10 19:49:15 +00001011 assert all(
1012 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
1013
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001014 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001015 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +00001016 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
1017 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +00001018 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001019
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001020 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001021 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001022 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001023
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024 def DescriptionText(self):
1025 """Returns the user-entered changelist description, minus tags.
1026
1027 Any line in the user-provided description starting with e.g. "FOO="
1028 (whitespace permitted before and around) is considered a tag line. Such
1029 lines are stripped out of the description this function returns.
1030 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001031 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001032
1033 def FullDescriptionText(self):
1034 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +00001035 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001036
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001037 def SetDescriptionText(self, description):
1038 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001039
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001040 Also updates the list of tags."""
1041 self._full_description = description
1042
1043 # From the description text, build up a dictionary of key/value pairs
1044 # plus the description minus all key/value or "tag" lines.
1045 description_without_tags = []
1046 self.tags = {}
1047 for line in self._full_description.splitlines():
1048 m = self.TAG_LINE_RE.match(line)
1049 if m:
1050 self.tags[m.group('key')] = m.group('value')
1051 else:
1052 description_without_tags.append(line)
1053
1054 # Change back to text and remove whitespace at end.
1055 self._description_without_tags = (
1056 '\n'.join(description_without_tags).rstrip())
1057
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001058 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001059 """Returns the repository (checkout) root directory for this change,
1060 as an absolute path.
1061 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001062 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001063
1064 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +00001065 """Return tags directly as attributes on the object."""
1066 if not re.match(r"^[A-Z_]*$", attr):
1067 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +00001068 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001069
Aaron Gablefc03e672017-05-15 14:09:42 -07001070 def BugsFromDescription(self):
1071 """Returns all bugs referenced in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -07001072 tags = [b.strip() for b in self.tags.get('BUG', '').split(',') if b.strip()]
1073 footers = git_footers.parse_footers(self._full_description).get('Bug', [])
1074 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -07001075
1076 def ReviewersFromDescription(self):
1077 """Returns all reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -07001078 # We don't support a "R:" git-footer for reviewers; that is in metadata.
1079 tags = [r.strip() for r in self.tags.get('R', '').split(',') if r.strip()]
1080 return sorted(set(tags))
Aaron Gablefc03e672017-05-15 14:09:42 -07001081
1082 def TBRsFromDescription(self):
1083 """Returns all TBR reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -07001084 tags = [r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()]
1085 # TODO(agable): Remove support for 'Tbr:' when TBRs are programmatically
1086 # determined by self-CR+1s.
1087 footers = git_footers.parse_footers(self._full_description).get('Tbr', [])
1088 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -07001089
1090 # TODO(agable): Delete these once we're sure they're unused.
1091 @property
1092 def BUG(self):
1093 return ','.join(self.BugsFromDescription())
1094 @property
1095 def R(self):
1096 return ','.join(self.ReviewersFromDescription())
1097 @property
1098 def TBR(self):
1099 return ','.join(self.TBRsFromDescription())
1100
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001101 def AllFiles(self, root=None):
1102 """List all files under source control in the repo."""
1103 raise NotImplementedError()
1104
agable0b65e732016-11-22 09:25:46 -08001105 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001106 """Returns a list of AffectedFile instances for all files in the change.
1107
1108 Args:
1109 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +00001110 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001111
1112 Returns:
1113 [AffectedFile(path, action), AffectedFile(path, action)]
1114 """
agable0b65e732016-11-22 09:25:46 -08001115 affected = filter(file_filter, self._affected_files)
sail@chromium.org5538e022011-05-12 17:53:16 +00001116
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001117 if include_deletes:
1118 return affected
Lei Zhang9611c4c2017-04-04 01:41:56 -07001119 return filter(lambda x: x.Action() != 'D', affected)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001120
agable0b65e732016-11-22 09:25:46 -08001121 def AffectedTestableFiles(self, include_deletes=None):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +00001122 """Return a list of the existing text files in a change."""
1123 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -08001124 warn("AffectedTeestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001125 " is deprecated and ignored" % str(include_deletes),
1126 category=DeprecationWarning,
1127 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -08001128 return filter(lambda x: x.IsTestableFile(),
1129 self.AffectedFiles(include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001130
agable0b65e732016-11-22 09:25:46 -08001131 def AffectedTextFiles(self, include_deletes=None):
1132 """An alias to AffectedTestableFiles for backwards compatibility."""
1133 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001134
agable0b65e732016-11-22 09:25:46 -08001135 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001136 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001137 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001138
agable0b65e732016-11-22 09:25:46 -08001139 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001140 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001141 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001142
1143 def RightHandSideLines(self):
1144 """An iterator over all text lines in "new" version of changed files.
1145
1146 Lists lines from new or modified text files in the change.
1147
1148 This is useful for doing line-by-line regex checks, like checking for
1149 trailing whitespace.
1150
1151 Yields:
1152 a 3 tuple:
1153 the AffectedFile instance of the current file;
1154 integer line number (1-based); and
1155 the contents of the line as a string.
1156 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001157 return _RightHandSideLinesImpl(
1158 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -08001159 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001160
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02001161 def OriginalOwnersFiles(self):
1162 """A map from path names of affected OWNERS files to their old content."""
1163 def owners_file_filter(f):
1164 return 'OWNERS' in os.path.split(f.LocalPath())[1]
1165 files = self.AffectedFiles(file_filter=owners_file_filter)
1166 return dict([(f.LocalPath(), f.OldContents()) for f in files])
1167
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001168
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001169class GitChange(Change):
1170 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001171 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001172
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001173 def AllFiles(self, root=None):
1174 """List all files under source control in the repo."""
1175 root = root or self.RepositoryRoot()
1176 return subprocess.check_output(
Aaron Gable7817f022017-12-12 09:43:17 -08001177 ['git', '-c', 'core.quotePath=false', 'ls-files', '--', '.'],
1178 cwd=root).splitlines()
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001179
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001180
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001181def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001182 """Finds all presubmit files that apply to a given set of source files.
1183
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001184 If inherit-review-settings-ok is present right under root, looks for
1185 PRESUBMIT.py in directories enclosing root.
1186
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001187 Args:
1188 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001189 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001190
1191 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001192 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001193 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001194 files = [normpath(os.path.join(root, f)) for f in files]
1195
1196 # List all the individual directories containing files.
1197 directories = set([os.path.dirname(f) for f in files])
1198
1199 # Ignore root if inherit-review-settings-ok is present.
1200 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1201 root = None
1202
1203 # Collect all unique directories that may contain PRESUBMIT.py.
1204 candidates = set()
1205 for directory in directories:
1206 while True:
1207 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001208 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001209 candidates.add(directory)
1210 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001211 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001212 parent_dir = os.path.dirname(directory)
1213 if parent_dir == directory:
1214 # We hit the system root directory.
1215 break
1216 directory = parent_dir
1217
1218 # Look for PRESUBMIT.py in all candidate directories.
1219 results = []
1220 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -07001221 try:
1222 for f in os.listdir(directory):
1223 p = os.path.join(directory, f)
1224 if os.path.isfile(p) and re.match(
1225 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1226 results.append(p)
1227 except OSError:
1228 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001229
tobiasjs2836bcf2016-08-16 04:08:16 -07001230 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001231 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001232
1233
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001234class GetTryMastersExecuter(object):
1235 @staticmethod
1236 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1237 """Executes GetPreferredTryMasters() from a single presubmit script.
1238
1239 Args:
1240 script_text: The text of the presubmit script.
1241 presubmit_path: Project script to run.
1242 project: Project name to pass to presubmit script for bot selection.
1243
1244 Return:
1245 A map of try masters to map of builders to set of tests.
1246 """
1247 context = {}
1248 try:
1249 exec script_text in context
1250 except Exception, e:
1251 raise PresubmitFailure('"%s" had an exception.\n%s'
1252 % (presubmit_path, e))
1253
1254 function_name = 'GetPreferredTryMasters'
1255 if function_name not in context:
1256 return {}
1257 get_preferred_try_masters = context[function_name]
1258 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1259 raise PresubmitFailure(
1260 'Expected function "GetPreferredTryMasters" to take two arguments.')
1261 return get_preferred_try_masters(project, change)
1262
1263
rmistry@google.com5626a922015-02-26 14:03:30 +00001264class GetPostUploadExecuter(object):
1265 @staticmethod
1266 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1267 """Executes PostUploadHook() from a single presubmit script.
1268
1269 Args:
1270 script_text: The text of the presubmit script.
1271 presubmit_path: Project script to run.
1272 cl: The Changelist object.
1273 change: The Change object.
1274
1275 Return:
1276 A list of results objects.
1277 """
1278 context = {}
1279 try:
1280 exec script_text in context
1281 except Exception, e:
1282 raise PresubmitFailure('"%s" had an exception.\n%s'
1283 % (presubmit_path, e))
1284
1285 function_name = 'PostUploadHook'
1286 if function_name not in context:
1287 return {}
1288 post_upload_hook = context[function_name]
1289 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1290 raise PresubmitFailure(
1291 'Expected function "PostUploadHook" to take three arguments.')
1292 return post_upload_hook(cl, change, OutputApi(False))
1293
1294
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001295def _MergeMasters(masters1, masters2):
1296 """Merges two master maps. Merges also the tests of each builder."""
1297 result = {}
1298 for (master, builders) in itertools.chain(masters1.iteritems(),
1299 masters2.iteritems()):
1300 new_builders = result.setdefault(master, {})
1301 for (builder, tests) in builders.iteritems():
1302 new_builders.setdefault(builder, set([])).update(tests)
1303 return result
1304
1305
1306def DoGetTryMasters(change,
1307 changed_files,
1308 repository_root,
1309 default_presubmit,
1310 project,
1311 verbose,
1312 output_stream):
1313 """Get the list of try masters from the presubmit scripts.
1314
1315 Args:
1316 changed_files: List of modified files.
1317 repository_root: The repository root.
1318 default_presubmit: A default presubmit script to execute in any case.
1319 project: Optional name of a project used in selecting trybots.
1320 verbose: Prints debug info.
1321 output_stream: A stream to write debug output to.
1322
1323 Return:
1324 Map of try masters to map of builders to set of tests.
1325 """
1326 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1327 if not presubmit_files and verbose:
1328 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1329 results = {}
1330 executer = GetTryMastersExecuter()
1331
1332 if default_presubmit:
1333 if verbose:
1334 output_stream.write("Running default presubmit script.\n")
1335 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1336 results = _MergeMasters(results, executer.ExecPresubmitScript(
1337 default_presubmit, fake_path, project, change))
1338 for filename in presubmit_files:
1339 filename = os.path.abspath(filename)
1340 if verbose:
1341 output_stream.write("Running %s\n" % filename)
1342 # Accept CRLF presubmit script.
1343 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1344 results = _MergeMasters(results, executer.ExecPresubmitScript(
1345 presubmit_script, filename, project, change))
1346
1347 # Make sets to lists again for later JSON serialization.
1348 for builders in results.itervalues():
1349 for builder in builders:
1350 builders[builder] = list(builders[builder])
1351
1352 if results and verbose:
1353 output_stream.write('%s\n' % str(results))
1354 return results
1355
1356
rmistry@google.com5626a922015-02-26 14:03:30 +00001357def DoPostUploadExecuter(change,
1358 cl,
1359 repository_root,
1360 verbose,
1361 output_stream):
1362 """Execute the post upload hook.
1363
1364 Args:
1365 change: The Change object.
1366 cl: The Changelist object.
1367 repository_root: The repository root.
1368 verbose: Prints debug info.
1369 output_stream: A stream to write debug output to.
1370 """
1371 presubmit_files = ListRelevantPresubmitFiles(
1372 change.LocalPaths(), repository_root)
1373 if not presubmit_files and verbose:
1374 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1375 results = []
1376 executer = GetPostUploadExecuter()
1377 # The root presubmit file should be executed after the ones in subdirectories.
1378 # i.e. the specific post upload hooks should run before the general ones.
1379 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1380 presubmit_files.reverse()
1381
1382 for filename in presubmit_files:
1383 filename = os.path.abspath(filename)
1384 if verbose:
1385 output_stream.write("Running %s\n" % filename)
1386 # Accept CRLF presubmit script.
1387 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1388 results.extend(executer.ExecPresubmitScript(
1389 presubmit_script, filename, cl, change))
1390 output_stream.write('\n')
1391 if results:
1392 output_stream.write('** Post Upload Hook Messages **\n')
1393 for result in results:
1394 result.handle(output_stream)
1395 output_stream.write('\n')
1396
1397 return results
1398
1399
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001400class PresubmitExecuter(object):
Aaron Gable668c1d82018-04-03 10:19:16 -07001401 def __init__(self, change, committing, verbose,
1402 gerrit_obj, dry_run=None, thread_pool=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001403 """
1404 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001405 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001406 committing: True if 'git cl land' is running, False if 'git cl upload' is.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001407 gerrit_obj: provides basic Gerrit codereview functionality.
1408 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001409 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001410 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001411 self.committing = committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001412 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001413 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001414 self.dry_run = dry_run
Daniel Cheng7227d212017-11-17 08:12:37 -08001415 self.more_cc = []
Edward Lesmesc7d0b342018-03-28 21:19:00 -04001416 self.thread_pool = thread_pool
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001417
1418 def ExecPresubmitScript(self, script_text, presubmit_path):
1419 """Executes a single presubmit script.
1420
1421 Args:
1422 script_text: The text of the presubmit script.
1423 presubmit_path: The path to the presubmit file (this will be reported via
1424 input_api.PresubmitLocalPath()).
1425
1426 Return:
1427 A list of result objects, empty if no problems.
1428 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001429
chase@chromium.org8e416c82009-10-06 04:30:44 +00001430 # Change to the presubmit file's directory to support local imports.
1431 main_path = os.getcwd()
1432 os.chdir(os.path.dirname(presubmit_path))
1433
1434 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001435 input_api = InputApi(self.change, presubmit_path, self.committing,
Aaron Gable668c1d82018-04-03 10:19:16 -07001436 self.verbose, gerrit_obj=self.gerrit,
1437 dry_run=self.dry_run, thread_pool=self.thread_pool)
Daniel Cheng7227d212017-11-17 08:12:37 -08001438 output_api = OutputApi(self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001439 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001440 try:
1441 exec script_text in context
1442 except Exception, e:
1443 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001444
1445 # These function names must change if we make substantial changes to
1446 # the presubmit API that are not backwards compatible.
1447 if self.committing:
1448 function_name = 'CheckChangeOnCommit'
1449 else:
1450 function_name = 'CheckChangeOnUpload'
1451 if function_name in context:
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +01001452 try:
Daniel Cheng7227d212017-11-17 08:12:37 -08001453 context['__args'] = (input_api, output_api)
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +01001454 logging.debug('Running %s in %s', function_name, presubmit_path)
1455 result = eval(function_name + '(*__args)', context)
1456 logging.debug('Running %s done.', function_name)
Daniel Chengd36fce42017-11-21 21:52:52 -08001457 self.more_cc.extend(output_api.more_cc)
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +01001458 finally:
1459 map(os.remove, input_api._named_temporary_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001460 if not (isinstance(result, types.TupleType) or
1461 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001462 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001463 'Presubmit functions must return a tuple or list')
1464 for item in result:
1465 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001466 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001467 'All presubmit results must be of types derived from '
1468 'output_api.PresubmitResult')
1469 else:
1470 result = () # no error since the script doesn't care about current event.
1471
chase@chromium.org8e416c82009-10-06 04:30:44 +00001472 # Return the process to the original working directory.
1473 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001474 return result
1475
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001476def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001477 committing,
1478 verbose,
1479 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001480 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001481 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001482 may_prompt,
Aaron Gable668c1d82018-04-03 10:19:16 -07001483 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001484 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001485 """Runs all presubmit checks that apply to the files in the change.
1486
1487 This finds all PRESUBMIT.py files in directories enclosing the files in the
1488 change (up to the repository root) and calls the relevant entrypoint function
1489 depending on whether the change is being committed or uploaded.
1490
1491 Prints errors, warnings and notifications. Prompts the user for warnings
1492 when needed.
1493
1494 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001495 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001496 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001497 verbose: Prints debug info.
1498 output_stream: A stream to write output from presubmit tests to.
1499 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001500 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001501 may_prompt: Enable (y/n) questions on warning or error. If False,
1502 any questions are answered with yes by default.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001503 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001504 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001505
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001506 Warning:
1507 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1508 SHOULD be sys.stdin.
1509
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001510 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001511 A PresubmitOutput object. Use output.should_continue() to figure out
1512 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001513 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001514 old_environ = os.environ
1515 try:
1516 # Make sure python subprocesses won't generate .pyc files.
1517 os.environ = os.environ.copy()
1518 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001519
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001520 output = PresubmitOutput(input_stream, output_stream)
1521 if committing:
1522 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001523 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001524 output.write("Running presubmit upload checks ...\n")
1525 start_time = time.time()
1526 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001527 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001528 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001529 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001530 results = []
Edward Lesmesc7d0b342018-03-28 21:19:00 -04001531 thread_pool = ThreadPool()
Aaron Gable668c1d82018-04-03 10:19:16 -07001532 executer = PresubmitExecuter(change, committing, verbose,
Edward Lesmesc7d0b342018-03-28 21:19:00 -04001533 gerrit_obj, dry_run, thread_pool)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001534 if default_presubmit:
1535 if verbose:
1536 output.write("Running default presubmit script.\n")
1537 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1538 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1539 for filename in presubmit_files:
1540 filename = os.path.abspath(filename)
1541 if verbose:
1542 output.write("Running %s\n" % filename)
1543 # Accept CRLF presubmit script.
1544 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1545 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001546
Edward Lesmesc7d0b342018-03-28 21:19:00 -04001547 results += thread_pool.RunAsync()
1548
Daniel Cheng7227d212017-11-17 08:12:37 -08001549 output.more_cc.extend(executer.more_cc)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001550 errors = []
1551 notifications = []
1552 warnings = []
1553 for result in results:
1554 if result.fatal:
1555 errors.append(result)
1556 elif result.should_prompt:
1557 warnings.append(result)
1558 else:
1559 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001560
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001561 output.write('\n')
1562 for name, items in (('Messages', notifications),
1563 ('Warnings', warnings),
1564 ('ERRORS', errors)):
1565 if items:
1566 output.write('** Presubmit %s **\n' % name)
1567 for item in items:
1568 item.handle(output)
1569 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001570
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001571 total_time = time.time() - start_time
1572 if total_time > 1.0:
1573 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001574
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001575 if errors:
1576 output.fail()
1577 elif warnings:
1578 output.write('There were presubmit warnings. ')
1579 if may_prompt:
1580 output.prompt_yes_no('Are you sure you wish to continue? (y/N): ')
1581 else:
1582 output.write('Presubmit checks passed.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001583
1584 global _ASKED_FOR_FEEDBACK
1585 # Ask for feedback one time out of 5.
1586 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001587 output.write(
1588 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1589 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1590 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001591 _ASKED_FOR_FEEDBACK = True
1592 return output
1593 finally:
1594 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001595
1596
1597def ScanSubDirs(mask, recursive):
1598 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001599 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001600
1601 results = []
1602 for root, dirs, files in os.walk('.'):
1603 if '.svn' in dirs:
1604 dirs.remove('.svn')
1605 if '.git' in dirs:
1606 dirs.remove('.git')
1607 for name in files:
1608 if fnmatch.fnmatch(name, mask):
1609 results.append(os.path.join(root, name))
1610 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001611
1612
1613def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001614 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001615 files = []
1616 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001617 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001618 return files
1619
1620
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001621def load_files(options, args):
1622 """Tries to determine the SCM."""
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001623 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001624 if args:
1625 files = ParseFiles(args, options.recursive)
agable0b65e732016-11-22 09:25:46 -08001626 change_scm = scm.determine_scm(options.root)
1627 if change_scm == 'git':
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001628 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001629 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001630 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001631 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001632 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001633 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001634 if not files:
1635 return None, None
1636 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001637 return change_class, files
1638
1639
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001640@contextlib.contextmanager
1641def canned_check_filter(method_names):
1642 filtered = {}
1643 try:
1644 for method_name in method_names:
1645 if not hasattr(presubmit_canned_checks, method_name):
Aaron Gableecee74c2018-04-02 15:13:08 -07001646 logging.warn('Skipping unknown "canned" check %s' % method_name)
1647 continue
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001648 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1649 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1650 yield
1651 finally:
1652 for name, method in filtered.iteritems():
1653 setattr(presubmit_canned_checks, name, method)
1654
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001655
sbc@chromium.org013731e2015-02-26 18:28:43 +00001656def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001657 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001658 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001659 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001660 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001661 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1662 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001663 parser.add_option("-r", "--recursive", action="store_true",
1664 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001665 parser.add_option("-v", "--verbose", action="count", default=0,
1666 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001667 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001668 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001669 parser.add_option("--description", default='')
1670 parser.add_option("--issue", type='int', default=0)
1671 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001672 parser.add_option("--root", default=os.getcwd(),
1673 help="Search for PRESUBMIT.py up to this directory. "
1674 "If inherit-review-settings-ok is present in this "
1675 "directory, parent directories up to the root file "
1676 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001677 parser.add_option("--upstream",
1678 help="Git only: the base ref or upstream branch against "
1679 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001680 parser.add_option("--default_presubmit")
1681 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001682 parser.add_option("--skip_canned", action='append', default=[],
1683 help="A list of checks to skip which appear in "
1684 "presubmit_canned_checks. Can be provided multiple times "
1685 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001686 parser.add_option("--dry_run", action='store_true',
1687 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001688 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001689 parser.add_option("--gerrit_fetch", action='store_true',
1690 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001691
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001692 options, args = parser.parse_args(argv)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001693
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001694 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001695 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001696 elif options.verbose:
1697 logging.basicConfig(level=logging.INFO)
1698 else:
1699 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001700
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001701 change_class, files = load_files(options, args)
1702 if not change_class:
1703 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001704 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001705
Aaron Gable668c1d82018-04-03 10:19:16 -07001706 gerrit_obj = None
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001707 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001708 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001709 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1710 options.author = gerrit_obj.GetChangeOwner(options.issue)
1711 options.description = gerrit_obj.GetChangeDescription(options.issue,
1712 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001713 logging.info('Got author: "%s"', options.author)
1714 logging.info('Got description: """\n%s\n"""', options.description)
1715
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001716 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001717 with canned_check_filter(options.skip_canned):
1718 results = DoPresubmitChecks(
1719 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001720 options.description,
1721 options.root,
1722 files,
1723 options.issue,
1724 options.patchset,
1725 options.author,
1726 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001727 options.commit,
1728 options.verbose,
1729 sys.stdout,
1730 sys.stdin,
1731 options.default_presubmit,
1732 options.may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001733 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001734 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001735 return not results.should_continue()
1736 except PresubmitFailure, e:
1737 print >> sys.stderr, e
1738 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001739 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001740
1741
1742if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001743 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001744 try:
1745 sys.exit(main())
1746 except KeyboardInterrupt:
1747 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001748 sys.exit(2)