blob: 5851178f11cc619791e7ab8c6f0d48e8495ce4cc [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
enne@chromium.orge72c5f52013-04-16 00:36:40 +000015import cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000016import cPickle # Exposed through the API.
17import cStringIO # Exposed through the API.
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +000018import contextlib
dcheng091b7db2016-06-16 01:27:51 -070019import fnmatch # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000020import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000021import inspect
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +000022import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000023import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000024import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000025import marshal # Exposed through the API.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000026import multiprocessing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000027import optparse
28import os # Somewhat exposed through the API.
29import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000030import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000031import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import sys # Parts exposed through API.
33import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000034import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000035import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000036import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000037import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000038import urllib2 # Exposed through the API.
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000039import urlparse
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000040from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000041
42# Local imports.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
maruel@chromium.org35625c72011-03-23 17:34:02 +000044import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000045import gclient_utils
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000046import gerrit_util
dpranke@chromium.org2a009622011-03-01 02:43:31 +000047import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000048import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000049import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000050import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000051import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052
53
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000054# Ask for feedback only once in program lifetime.
55_ASKED_FOR_FEEDBACK = False
56
57
maruel@chromium.org899e1c12011-04-07 17:03:18 +000058class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000059 pass
60
61
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000062class CommandData(object):
63 def __init__(self, name, cmd, kwargs, message):
64 self.name = name
65 self.cmd = cmd
66 self.kwargs = kwargs
67 self.message = message
68 self.info = None
69
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000070
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000071def normpath(path):
72 '''Version of os.path.normpath that also changes backward slashes to
73 forward slashes when not running on Windows.
74 '''
75 # This is safe to always do because the Windows version of os.path.normpath
76 # will replace forward slashes with backward slashes.
77 path = path.replace(os.sep, '/')
78 return os.path.normpath(path)
79
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000080
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000081def _RightHandSideLinesImpl(affected_files):
82 """Implements RightHandSideLines for InputApi and GclChange."""
83 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000084 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000085 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000086 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000087
88
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000089class PresubmitOutput(object):
90 def __init__(self, input_stream=None, output_stream=None):
91 self.input_stream = input_stream
92 self.output_stream = output_stream
93 self.reviewers = []
94 self.written_output = []
95 self.error_count = 0
96
97 def prompt_yes_no(self, prompt_string):
98 self.write(prompt_string)
99 if self.input_stream:
100 response = self.input_stream.readline().strip().lower()
101 if response not in ('y', 'yes'):
102 self.fail()
103 else:
104 self.fail()
105
106 def fail(self):
107 self.error_count += 1
108
109 def should_continue(self):
110 return not self.error_count
111
112 def write(self, s):
113 self.written_output.append(s)
114 if self.output_stream:
115 self.output_stream.write(s)
116
117 def getvalue(self):
118 return ''.join(self.written_output)
119
120
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000121# Top level object so multiprocessing can pickle
122# Public access through OutputApi object.
123class _PresubmitResult(object):
124 """Base class for result objects."""
125 fatal = False
126 should_prompt = False
127
128 def __init__(self, message, items=None, long_text=''):
129 """
130 message: A short one-line message to indicate errors.
131 items: A list of short strings to indicate where errors occurred.
132 long_text: multi-line text output, e.g. from another tool
133 """
134 self._message = message
135 self._items = items or []
136 if items:
137 self._items = items
138 self._long_text = long_text.rstrip()
139
140 def handle(self, output):
141 output.write(self._message)
142 output.write('\n')
143 for index, item in enumerate(self._items):
144 output.write(' ')
145 # Write separately in case it's unicode.
146 output.write(str(item))
147 if index < len(self._items) - 1:
148 output.write(' \\')
149 output.write('\n')
150 if self._long_text:
151 output.write('\n***************\n')
152 # Write separately in case it's unicode.
153 output.write(self._long_text)
154 output.write('\n***************\n')
155 if self.fatal:
156 output.fail()
157
158
159# Top level object so multiprocessing can pickle
160# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000161class _PresubmitError(_PresubmitResult):
162 """A hard presubmit error."""
163 fatal = True
164
165
166# Top level object so multiprocessing can pickle
167# Public access through OutputApi object.
168class _PresubmitPromptWarning(_PresubmitResult):
169 """An warning that prompts the user if they want to continue."""
170 should_prompt = True
171
172
173# Top level object so multiprocessing can pickle
174# Public access through OutputApi object.
175class _PresubmitNotifyResult(_PresubmitResult):
176 """Just print something to the screen -- but it's not even a warning."""
177 pass
178
179
180# Top level object so multiprocessing can pickle
181# Public access through OutputApi object.
182class _MailTextResult(_PresubmitResult):
183 """A warning that should be included in the review request email."""
184 def __init__(self, *args, **kwargs):
185 super(_MailTextResult, self).__init__()
186 raise NotImplementedError()
187
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000188class GerritAccessor(object):
189 """Limited Gerrit functionality for canned presubmit checks to work.
190
191 To avoid excessive Gerrit calls, caches the results.
192 """
193
194 def __init__(self, host):
195 self.host = host
196 self.cache = {}
197
198 def _FetchChangeDetail(self, issue):
199 # Separate function to be easily mocked in tests.
200 return gerrit_util.GetChangeDetail(
201 self.host, str(issue),
202 ['ALL_REVISIONS', 'DETAILED_LABELS'])
203
204 def GetChangeInfo(self, issue):
205 """Returns labels and all revisions (patchsets) for this issue.
206
207 The result is a dictionary according to Gerrit REST Api.
208 https://gerrit-review.googlesource.com/Documentation/rest-api.html
209
210 However, API isn't very clear what's inside, so see tests for example.
211 """
212 assert issue
213 cache_key = int(issue)
214 if cache_key not in self.cache:
215 self.cache[cache_key] = self._FetchChangeDetail(issue)
216 return self.cache[cache_key]
217
218 def GetChangeDescription(self, issue, patchset=None):
219 """If patchset is none, fetches current patchset."""
220 info = self.GetChangeInfo(issue)
221 # info is a reference to cache. We'll modify it here adding description to
222 # it to the right patchset, if it is not yet there.
223
224 # Find revision info for the patchset we want.
225 if patchset is not None:
226 for rev, rev_info in info['revisions'].iteritems():
227 if str(rev_info['_number']) == str(patchset):
228 break
229 else:
230 raise Exception('patchset %s doesn\'t exist in issue %s' % (
231 patchset, issue))
232 else:
233 rev = info['current_revision']
234 rev_info = info['revisions'][rev]
235
236 # Updates revision info, which is part of cached issue info.
237 if 'real_description' not in rev_info:
238 rev_info['real_description'] = (
239 gerrit_util.GetChangeDescriptionFromGitiles(
240 rev_info['fetch']['http']['url'], rev))
241 return rev_info['real_description']
242
243 def GetChangeOwner(self, issue):
244 return self.GetChangeInfo(issue)['owner']['email']
245
246 def GetChangeReviewers(self, issue, approving_only=True):
agable565adb52016-07-22 14:48:07 -0700247 cr = self.GetChangeInfo(issue)['labels']['Code-Review']
248 max_value = max(int(k) for k in cr['values'].keys())
249 return [r['email'] for r in cr['all']
250 if not approving_only or r.get('value', 0) == max_value]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000251
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000252
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000253class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000254 """An instance of OutputApi gets passed to presubmit scripts so that they
255 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000257 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000258 PresubmitError = _PresubmitError
259 PresubmitPromptWarning = _PresubmitPromptWarning
260 PresubmitNotifyResult = _PresubmitNotifyResult
261 MailTextResult = _MailTextResult
262
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000263 def __init__(self, is_committing):
264 self.is_committing = is_committing
265
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000266 def PresubmitPromptOrNotify(self, *args, **kwargs):
267 """Warn the user when uploading, but only notify if committing."""
268 if self.is_committing:
269 return self.PresubmitNotifyResult(*args, **kwargs)
270 return self.PresubmitPromptWarning(*args, **kwargs)
271
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000272
273class InputApi(object):
274 """An instance of this object is passed to presubmit scripts so they can
275 know stuff about the change they're looking at.
276 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000277 # Method could be a function
278 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000279
maruel@chromium.org3410d912009-06-09 20:56:16 +0000280 # File extensions that are considered source files from a style guide
281 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000282 #
283 # Files without an extension aren't included in the list. If you want to
284 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
285 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000286 DEFAULT_WHITE_LIST = (
287 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000288 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
289 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000290 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000291 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000292 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000293 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000294 )
295
296 # Path regexp that should be excluded from being considered containing source
297 # files. Don't modify this list from a presubmit script!
298 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000299 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000300 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000301 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
302 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000303 # Output directories (just in case)
304 r".*\bDebug[\\\/].*",
305 r".*\bRelease[\\\/].*",
306 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000307 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000308 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000309 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000310 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000311 r"(|.*[\\\/])\.git[\\\/].*",
312 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000313 # There is no point in processing a patch file.
314 r".+\.diff$",
315 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000316 )
317
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000318 def __init__(self, change, presubmit_path, is_committing,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000319 rietveld_obj, verbose, gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000320 """Builds an InputApi object.
321
322 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000323 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000324 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000325 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000326 rietveld_obj: rietveld.Rietveld client object
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000327 gerrit_obj: provides basic Gerrit codereview functionality.
328 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000329 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000330 # Version number of the presubmit_support script.
331 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000332 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000333 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000334 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000335 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000336 self.dry_run = dry_run
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000337 # TBD
338 self.host_url = 'http://codereview.chromium.org'
339 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000340 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000341
342 # We expose various modules and functions as attributes of the input_api
343 # so that presubmit scripts don't have to import them.
344 self.basename = os.path.basename
345 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000346 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000347 self.cStringIO = cStringIO
dcheng091b7db2016-06-16 01:27:51 -0700348 self.fnmatch = fnmatch
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000349 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000350 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000351 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000352 self.os_listdir = os.listdir
353 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000354 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000355 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000356 self.pickle = pickle
357 self.marshal = marshal
358 self.re = re
359 self.subprocess = subprocess
360 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000361 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000362 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000363 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000364 self.urllib2 = urllib2
365
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000366 # To easily fork python.
367 self.python_executable = sys.executable
368 self.environ = os.environ
369
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000370 # InputApi.platform is the platform you're currently running on.
371 self.platform = sys.platform
372
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000373 self.cpu_count = multiprocessing.cpu_count()
374
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000375 # this is done here because in RunTests, the current working directory has
376 # changed, which causes Pool() to explode fantastically when run on windows
377 # (because it tries to load the __main__ module, which imports lots of
378 # things relative to the current working directory).
379 self._run_tests_pool = multiprocessing.Pool(self.cpu_count)
380
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000381 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000382 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000383
384 # We carry the canned checks so presubmit scripts can easily use them.
385 self.canned_checks = presubmit_canned_checks
386
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000387 # TODO(dpranke): figure out a list of all approved owners for a repo
388 # in order to be able to handle wildcard OWNERS files?
389 self.owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -0700390 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000391 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000392 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000393
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000394 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000395 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000396 # Access to a protected member _XX of a client class
397 # pylint: disable=W0212
398 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000399 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000400 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
401 for (a, b, header) in cpplint._re_pattern_templates
402 ]
403
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000404 def PresubmitLocalPath(self):
405 """Returns the local path of the presubmit script currently being run.
406
407 This is useful if you don't want to hard-code absolute paths in the
408 presubmit script. For example, It can be used to find another file
409 relative to the PRESUBMIT.py script, so the whole tree can be branched and
410 the presubmit script still works, without editing its content.
411 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000412 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000413
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000414 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000415 """Translate a depot path to a local path (relative to client root).
416
417 Args:
418 Depot path as a string.
419
420 Returns:
421 The local path of the depot path under the user's current client, or None
422 if the file is not mapped.
423
424 Remember to check for the None case and show an appropriate error!
425 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000426 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
427 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000428
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000429 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000430 """Translate a local path to a depot path.
431
432 Args:
433 Local path (relative to current directory, or absolute) as a string.
434
435 Returns:
436 The depot path (SVN URL) of the file if mapped, otherwise None.
437 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000438 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
439 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000440
sail@chromium.org5538e022011-05-12 17:53:16 +0000441 def AffectedFiles(self, include_dirs=False, include_deletes=True,
442 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000443 """Same as input_api.change.AffectedFiles() except only lists files
444 (and optionally directories) in the same directory as the current presubmit
445 script, or subdirectories thereof.
446 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000447 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000448 if len(dir_with_slash) == 1:
449 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000450
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000451 return filter(
452 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000453 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000454
455 def LocalPaths(self, include_dirs=False):
456 """Returns local paths of input_api.AffectedFiles()."""
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000457 paths = [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
458 logging.debug("LocalPaths: %s", paths)
459 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000460
461 def AbsoluteLocalPaths(self, include_dirs=False):
462 """Returns absolute local paths of input_api.AffectedFiles()."""
463 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
464
465 def ServerPaths(self, include_dirs=False):
466 """Returns server paths of input_api.AffectedFiles()."""
467 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
468
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000469 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000470 """Same as input_api.change.AffectedTextFiles() except only lists files
471 in the same directory as the current presubmit script, or subdirectories
472 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000473 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000474 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000475 warn("AffectedTextFiles(include_deletes=%s)"
476 " is deprecated and ignored" % str(include_deletes),
477 category=DeprecationWarning,
478 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000479 return filter(lambda x: x.IsTextFile(),
480 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000481
maruel@chromium.org3410d912009-06-09 20:56:16 +0000482 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
483 """Filters out files that aren't considered "source file".
484
485 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
486 and InputApi.DEFAULT_BLACK_LIST is used respectively.
487
488 The lists will be compiled as regular expression and
489 AffectedFile.LocalPath() needs to pass both list.
490
491 Note: Copy-paste this function to suit your needs or use a lambda function.
492 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000493 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000494 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000495 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000496 if self.re.match(item, local_path):
tobiasjs2836bcf2016-08-16 04:08:16 -0700497 logging.debug("%s matched %s", item, local_path)
maruel@chromium.org3410d912009-06-09 20:56:16 +0000498 return True
499 return False
500 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
501 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
502
503 def AffectedSourceFiles(self, source_file):
504 """Filter the list of AffectedTextFiles by the function source_file.
505
506 If source_file is None, InputApi.FilterSourceFile() is used.
507 """
508 if not source_file:
509 source_file = self.FilterSourceFile
510 return filter(source_file, self.AffectedTextFiles())
511
512 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000513 """An iterator over all text lines in "new" version of changed files.
514
515 Only lists lines from new or modified text files in the change that are
516 contained by the directory of the currently executing presubmit script.
517
518 This is useful for doing line-by-line regex checks, like checking for
519 trailing whitespace.
520
521 Yields:
522 a 3 tuple:
523 the AffectedFile instance of the current file;
524 integer line number (1-based); and
525 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000526
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000527 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000528 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000529 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000530 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000531
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000532 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000533 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000534
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000535 Deny reading anything outside the repository.
536 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000537 if isinstance(file_item, AffectedFile):
538 file_item = file_item.AbsoluteLocalPath()
539 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000540 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000541 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000542
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000543 @property
544 def tbr(self):
545 """Returns if a change is TBR'ed."""
546 return 'TBR' in self.change.tags
547
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000548 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000549 tests = []
550 msgs = []
551 for t in tests_mix:
552 if isinstance(t, OutputApi.PresubmitResult):
553 msgs.append(t)
554 else:
555 assert issubclass(t.message, _PresubmitResult)
556 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000557 if self.verbose:
558 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000559 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000560 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000561 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000562 else:
563 msgs.extend(map(CallCommand, tests))
564 return [m for m in msgs if m]
565
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000566
nick@chromium.orgff526192013-06-10 19:30:26 +0000567class _DiffCache(object):
568 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000569 def __init__(self, upstream=None):
570 """Stores the upstream revision against which all diffs will be computed."""
571 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000572
573 def GetDiff(self, path, local_root):
574 """Get the diff for a particular path."""
575 raise NotImplementedError()
576
577
578class _SvnDiffCache(_DiffCache):
579 """DiffCache implementation for subversion."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000580 def __init__(self, *args, **kwargs):
581 super(_SvnDiffCache, self).__init__(*args, **kwargs)
nick@chromium.orgff526192013-06-10 19:30:26 +0000582 self._diffs_by_file = {}
583
584 def GetDiff(self, path, local_root):
585 if path not in self._diffs_by_file:
586 self._diffs_by_file[path] = scm.SVN.GenerateDiff([path], local_root,
587 False, None)
588 return self._diffs_by_file[path]
589
590
591class _GitDiffCache(_DiffCache):
592 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000593 def __init__(self, upstream):
594 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000595 self._diffs_by_file = None
596
597 def GetDiff(self, path, local_root):
598 if not self._diffs_by_file:
599 # Compute a single diff for all files and parse the output; should
600 # with git this is much faster than computing one diff for each file.
601 diffs = {}
602
603 # Don't specify any filenames below, because there are command line length
604 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000605 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
606 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000607
608 # This regex matches the path twice, separated by a space. Note that
609 # filename itself may contain spaces.
610 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
611 current_diff = []
612 keep_line_endings = True
613 for x in unified_diff.splitlines(keep_line_endings):
614 match = file_marker.match(x)
615 if match:
616 # Marks the start of a new per-file section.
617 diffs[match.group('filename')] = current_diff = [x]
618 elif x.startswith('diff --git'):
619 raise PresubmitFailure('Unexpected diff line: %s' % x)
620 else:
621 current_diff.append(x)
622
623 self._diffs_by_file = dict(
624 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
625
626 if path not in self._diffs_by_file:
627 raise PresubmitFailure(
628 'Unified diff did not contain entry for file %s' % path)
629
630 return self._diffs_by_file[path]
631
632
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000633class AffectedFile(object):
634 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000635
636 DIFF_CACHE = _DiffCache
637
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000638 # Method could be a function
639 # pylint: disable=R0201
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000640 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000641 self._path = path
642 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000643 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000644 self._is_directory = None
645 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000646 self._cached_changed_contents = None
647 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000648 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700649 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000650
651 def ServerPath(self):
652 """Returns a path string that identifies the file in the SCM system.
653
654 Returns the empty string if the file does not exist in SCM.
655 """
nick@chromium.orgff526192013-06-10 19:30:26 +0000656 return ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000657
658 def LocalPath(self):
659 """Returns the path of this file on the local disk relative to client root.
660 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000661 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000662
663 def AbsoluteLocalPath(self):
664 """Returns the absolute path of this file on the local disk.
665 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000666 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000667
668 def IsDirectory(self):
669 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000670 if self._is_directory is None:
671 path = self.AbsoluteLocalPath()
672 self._is_directory = (os.path.exists(path) and
673 os.path.isdir(path))
674 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000675
676 def Action(self):
677 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000678 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
679 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000680 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000681
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000682 def Property(self, property_name):
683 """Returns the specified SCM property of this file, or None if no such
684 property.
685 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000686 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000687
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000688 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000689 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000690
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000691 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000692 raise NotImplementedError() # Implement when needed
693
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000694 def NewContents(self):
695 """Returns an iterator over the lines in the new version of file.
696
697 The new version is the file in the user's workspace, i.e. the "right hand
698 side".
699
700 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000701 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000702 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000703 if self._cached_new_contents is None:
704 self._cached_new_contents = []
705 if not self.IsDirectory():
706 try:
707 self._cached_new_contents = gclient_utils.FileRead(
708 self.AbsoluteLocalPath(), 'rU').splitlines()
709 except IOError:
710 pass # File not found? That's fine; maybe it was deleted.
711 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000712
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000713 def ChangedContents(self):
714 """Returns a list of tuples (line number, line text) of all new lines.
715
716 This relies on the scm diff output describing each changed code section
717 with a line of the form
718
719 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
720 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000721 if self._cached_changed_contents is not None:
722 return self._cached_changed_contents[:]
723 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000724 line_num = 0
725
726 if self.IsDirectory():
727 return []
728
729 for line in self.GenerateScmDiff().splitlines():
730 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
731 if m:
732 line_num = int(m.groups(1)[0])
733 continue
734 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000735 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000736 if not line.startswith('-'):
737 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000738 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000739
maruel@chromium.org5de13972009-06-10 18:16:06 +0000740 def __str__(self):
741 return self.LocalPath()
742
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000743 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000744 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000745
maruel@chromium.org58407af2011-04-12 23:15:57 +0000746
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000747class SvnAffectedFile(AffectedFile):
748 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000749 # Method 'NNN' is abstract in class 'NNN' but is not overridden
750 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000751
nick@chromium.orgff526192013-06-10 19:30:26 +0000752 DIFF_CACHE = _SvnDiffCache
753
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000754 def __init__(self, *args, **kwargs):
755 AffectedFile.__init__(self, *args, **kwargs)
756 self._server_path = None
757 self._is_text_file = None
758
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000759 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000760 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000761 self._server_path = scm.SVN.CaptureLocalInfo(
762 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000763 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000764
765 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000766 if self._is_directory is None:
767 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000768 if os.path.exists(path):
769 # Retrieve directly from the file system; it is much faster than
770 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000771 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000772 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000773 self._is_directory = scm.SVN.CaptureLocalInfo(
774 [self.LocalPath()], self._local_root
775 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000776 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000777
778 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000779 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000780 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000781 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000782 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000783
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000784 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000785 if self._is_text_file is None:
786 if self.Action() == 'D':
787 # A deleted file is not a text file.
788 self._is_text_file = False
789 elif self.IsDirectory():
790 self._is_text_file = False
791 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000792 mime_type = scm.SVN.GetFileProperty(
793 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000794 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
795 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000796
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000797
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000798class GitAffectedFile(AffectedFile):
799 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000800 # Method 'NNN' is abstract in class 'NNN' but is not overridden
801 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000802
nick@chromium.orgff526192013-06-10 19:30:26 +0000803 DIFF_CACHE = _GitDiffCache
804
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000805 def __init__(self, *args, **kwargs):
806 AffectedFile.__init__(self, *args, **kwargs)
807 self._server_path = None
808 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000809
810 def ServerPath(self):
811 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000812 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000813 return self._server_path
814
815 def IsDirectory(self):
816 if self._is_directory is None:
817 path = self.AbsoluteLocalPath()
818 if os.path.exists(path):
819 # Retrieve directly from the file system; it is much faster than
820 # querying subversion, especially on Windows.
821 self._is_directory = os.path.isdir(path)
822 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000823 self._is_directory = False
824 return self._is_directory
825
826 def Property(self, property_name):
827 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000828 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000829 return self._properties[property_name]
830
831 def IsTextFile(self):
832 if self._is_text_file is None:
833 if self.Action() == 'D':
834 # A deleted file is not a text file.
835 self._is_text_file = False
836 elif self.IsDirectory():
837 self._is_text_file = False
838 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000839 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
840 return self._is_text_file
841
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000842
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000843class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000844 """Describe a change.
845
846 Used directly by the presubmit scripts to query the current change being
847 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000848
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000849 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000850 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000851 self.KEY: equivalent to tags['KEY']
852 """
853
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000854 _AFFECTED_FILES = AffectedFile
855
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000856 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000857 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000858 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000859 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000860
maruel@chromium.org58407af2011-04-12 23:15:57 +0000861 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000862 self, name, description, local_root, files, issue, patchset, author,
863 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000864 if files is None:
865 files = []
866 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000867 # Convert root into an absolute path.
868 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000869 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000870 self.issue = issue
871 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000872 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000873
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000874 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000875 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000876 self._description_without_tags = ''
877 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000878
maruel@chromium.orge085d812011-10-10 19:49:15 +0000879 assert all(
880 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
881
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000882 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000883 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000884 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
885 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000886 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000887
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000888 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000889 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000890 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000891
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000892 def DescriptionText(self):
893 """Returns the user-entered changelist description, minus tags.
894
895 Any line in the user-provided description starting with e.g. "FOO="
896 (whitespace permitted before and around) is considered a tag line. Such
897 lines are stripped out of the description this function returns.
898 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000899 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000900
901 def FullDescriptionText(self):
902 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000903 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000904
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000905 def SetDescriptionText(self, description):
906 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000907
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000908 Also updates the list of tags."""
909 self._full_description = description
910
911 # From the description text, build up a dictionary of key/value pairs
912 # plus the description minus all key/value or "tag" lines.
913 description_without_tags = []
914 self.tags = {}
915 for line in self._full_description.splitlines():
916 m = self.TAG_LINE_RE.match(line)
917 if m:
918 self.tags[m.group('key')] = m.group('value')
919 else:
920 description_without_tags.append(line)
921
922 # Change back to text and remove whitespace at end.
923 self._description_without_tags = (
924 '\n'.join(description_without_tags).rstrip())
925
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000926 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000927 """Returns the repository (checkout) root directory for this change,
928 as an absolute path.
929 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000930 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000931
932 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000933 """Return tags directly as attributes on the object."""
934 if not re.match(r"^[A-Z_]*$", attr):
935 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000936 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000937
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000938 def AllFiles(self, root=None):
939 """List all files under source control in the repo."""
940 raise NotImplementedError()
941
sail@chromium.org5538e022011-05-12 17:53:16 +0000942 def AffectedFiles(self, include_dirs=False, include_deletes=True,
943 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000944 """Returns a list of AffectedFile instances for all files in the change.
945
946 Args:
947 include_deletes: If false, deleted files will be filtered out.
948 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000949 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000950
951 Returns:
952 [AffectedFile(path, action), AffectedFile(path, action)]
953 """
954 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000955 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000956 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000957 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000958
sail@chromium.org5538e022011-05-12 17:53:16 +0000959 affected = filter(file_filter, affected)
960
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000961 if include_deletes:
962 return affected
963 else:
964 return filter(lambda x: x.Action() != 'D', affected)
965
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000966 def AffectedTextFiles(self, include_deletes=None):
967 """Return a list of the existing text files in a change."""
968 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000969 warn("AffectedTextFiles(include_deletes=%s)"
970 " is deprecated and ignored" % str(include_deletes),
971 category=DeprecationWarning,
972 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000973 return filter(lambda x: x.IsTextFile(),
974 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000975
976 def LocalPaths(self, include_dirs=False):
977 """Convenience function."""
978 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
979
980 def AbsoluteLocalPaths(self, include_dirs=False):
981 """Convenience function."""
982 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
983
984 def ServerPaths(self, include_dirs=False):
985 """Convenience function."""
986 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
987
988 def RightHandSideLines(self):
989 """An iterator over all text lines in "new" version of changed files.
990
991 Lists lines from new or modified text files in the change.
992
993 This is useful for doing line-by-line regex checks, like checking for
994 trailing whitespace.
995
996 Yields:
997 a 3 tuple:
998 the AffectedFile instance of the current file;
999 integer line number (1-based); and
1000 the contents of the line as a string.
1001 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001002 return _RightHandSideLinesImpl(
1003 x for x in self.AffectedFiles(include_deletes=False)
1004 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001005
1006
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001007class SvnChange(Change):
1008 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001009 scm = 'svn'
1010 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +00001011
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001012 def AllFiles(self, root=None):
1013 """List all files under source control in the repo."""
1014 root = root or self.RepositoryRoot()
1015 return subprocess.check_output(
1016 ['svn', 'ls', '-R', '.'], cwd=root).splitlines()
1017
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001018
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001019class GitChange(Change):
1020 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001021 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001022
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001023 def AllFiles(self, root=None):
1024 """List all files under source control in the repo."""
1025 root = root or self.RepositoryRoot()
1026 return subprocess.check_output(
1027 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
1028
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001029
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001030def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001031 """Finds all presubmit files that apply to a given set of source files.
1032
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001033 If inherit-review-settings-ok is present right under root, looks for
1034 PRESUBMIT.py in directories enclosing root.
1035
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001036 Args:
1037 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001038 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001039
1040 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001041 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001042 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001043 files = [normpath(os.path.join(root, f)) for f in files]
1044
1045 # List all the individual directories containing files.
1046 directories = set([os.path.dirname(f) for f in files])
1047
1048 # Ignore root if inherit-review-settings-ok is present.
1049 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1050 root = None
1051
1052 # Collect all unique directories that may contain PRESUBMIT.py.
1053 candidates = set()
1054 for directory in directories:
1055 while True:
1056 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001057 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001058 candidates.add(directory)
1059 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001060 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001061 parent_dir = os.path.dirname(directory)
1062 if parent_dir == directory:
1063 # We hit the system root directory.
1064 break
1065 directory = parent_dir
1066
1067 # Look for PRESUBMIT.py in all candidate directories.
1068 results = []
1069 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -07001070 try:
1071 for f in os.listdir(directory):
1072 p = os.path.join(directory, f)
1073 if os.path.isfile(p) and re.match(
1074 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1075 results.append(p)
1076 except OSError:
1077 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001078
tobiasjs2836bcf2016-08-16 04:08:16 -07001079 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001080 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001081
1082
thestig@chromium.orgde243452009-10-06 21:02:56 +00001083class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001084 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001085 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +00001086 """Executes GetPreferredTrySlaves() from a single presubmit script.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001087
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001088 This will soon be deprecated and replaced by GetPreferredTryMasters().
thestig@chromium.orgde243452009-10-06 21:02:56 +00001089
1090 Args:
1091 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +00001092 presubmit_path: Project script to run.
1093 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001094
1095 Return:
1096 A list of try slaves.
1097 """
1098 context = {}
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001099 main_path = os.getcwd()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001100 try:
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001101 os.chdir(os.path.dirname(presubmit_path))
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001102 exec script_text in context
1103 except Exception, e:
1104 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001105 finally:
1106 os.chdir(main_path)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001107
1108 function_name = 'GetPreferredTrySlaves'
1109 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001110 get_preferred_try_slaves = context[function_name]
1111 function_info = inspect.getargspec(get_preferred_try_slaves)
1112 if len(function_info[0]) == 1:
1113 result = get_preferred_try_slaves(project)
1114 elif len(function_info[0]) == 2:
1115 result = get_preferred_try_slaves(project, change)
1116 else:
1117 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +00001118 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001119 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +00001120 'Presubmit functions must return a list, got a %s instead: %s' %
1121 (type(result), str(result)))
1122 for item in result:
stip@chromium.org68e04192013-11-04 22:14:38 +00001123 if isinstance(item, basestring):
1124 # Old-style ['bot'] format.
1125 botname = item
1126 elif isinstance(item, tuple):
1127 # New-style [('bot', set(['tests']))] format.
1128 botname = item[0]
1129 else:
1130 raise PresubmitFailure('PRESUBMIT.py returned invalid tryslave/test'
1131 ' format.')
1132
1133 if botname != botname.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001134 raise PresubmitFailure(
1135 'Try slave names cannot start/end with whitespace')
stip@chromium.org68e04192013-11-04 22:14:38 +00001136 if ',' in botname:
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +00001137 raise PresubmitFailure(
stip@chromium.org68e04192013-11-04 22:14:38 +00001138 'Do not use \',\' separated builder or test names: %s' % botname)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001139 else:
1140 result = []
stip@chromium.org5ca27622013-12-18 17:44:58 +00001141
1142 def valid_oldstyle(result):
1143 return all(isinstance(i, basestring) for i in result)
1144
1145 def valid_newstyle(result):
1146 return (all(isinstance(i, tuple) for i in result) and
1147 all(len(i) == 2 for i in result) and
1148 all(isinstance(i[0], basestring) for i in result) and
1149 all(isinstance(i[1], set) for i in result)
1150 )
1151
1152 # Ensure it's either all old-style or all new-style.
1153 if not valid_oldstyle(result) and not valid_newstyle(result):
1154 raise PresubmitFailure(
1155 'PRESUBMIT.py returned invalid trybot specification!')
1156
thestig@chromium.orgde243452009-10-06 21:02:56 +00001157 return result
1158
1159
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001160class GetTryMastersExecuter(object):
1161 @staticmethod
1162 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1163 """Executes GetPreferredTryMasters() from a single presubmit script.
1164
1165 Args:
1166 script_text: The text of the presubmit script.
1167 presubmit_path: Project script to run.
1168 project: Project name to pass to presubmit script for bot selection.
1169
1170 Return:
1171 A map of try masters to map of builders to set of tests.
1172 """
1173 context = {}
1174 try:
1175 exec script_text in context
1176 except Exception, e:
1177 raise PresubmitFailure('"%s" had an exception.\n%s'
1178 % (presubmit_path, e))
1179
1180 function_name = 'GetPreferredTryMasters'
1181 if function_name not in context:
1182 return {}
1183 get_preferred_try_masters = context[function_name]
1184 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1185 raise PresubmitFailure(
1186 'Expected function "GetPreferredTryMasters" to take two arguments.')
1187 return get_preferred_try_masters(project, change)
1188
1189
rmistry@google.com5626a922015-02-26 14:03:30 +00001190class GetPostUploadExecuter(object):
1191 @staticmethod
1192 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1193 """Executes PostUploadHook() from a single presubmit script.
1194
1195 Args:
1196 script_text: The text of the presubmit script.
1197 presubmit_path: Project script to run.
1198 cl: The Changelist object.
1199 change: The Change object.
1200
1201 Return:
1202 A list of results objects.
1203 """
1204 context = {}
1205 try:
1206 exec script_text in context
1207 except Exception, e:
1208 raise PresubmitFailure('"%s" had an exception.\n%s'
1209 % (presubmit_path, e))
1210
1211 function_name = 'PostUploadHook'
1212 if function_name not in context:
1213 return {}
1214 post_upload_hook = context[function_name]
1215 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1216 raise PresubmitFailure(
1217 'Expected function "PostUploadHook" to take three arguments.')
1218 return post_upload_hook(cl, change, OutputApi(False))
1219
1220
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001221def DoGetTrySlaves(change,
1222 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001223 repository_root,
1224 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +00001225 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001226 verbose,
1227 output_stream):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001228 """Get the list of try servers from the presubmit scripts (deprecated).
thestig@chromium.orgde243452009-10-06 21:02:56 +00001229
1230 Args:
1231 changed_files: List of modified files.
1232 repository_root: The repository root.
1233 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +00001234 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001235 verbose: Prints debug info.
1236 output_stream: A stream to write debug output to.
1237
1238 Return:
1239 List of try slaves
1240 """
1241 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1242 if not presubmit_files and verbose:
mdempsky@chromium.orgd59e7612014-03-05 19:55:56 +00001243 output_stream.write("Warning, no PRESUBMIT.py found.\n")
thestig@chromium.orgde243452009-10-06 21:02:56 +00001244 results = []
1245 executer = GetTrySlavesExecuter()
stip@chromium.org5ca27622013-12-18 17:44:58 +00001246
thestig@chromium.orgde243452009-10-06 21:02:56 +00001247 if default_presubmit:
1248 if verbose:
1249 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001250 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001251 results.extend(executer.ExecPresubmitScript(
1252 default_presubmit, fake_path, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001253 for filename in presubmit_files:
1254 filename = os.path.abspath(filename)
1255 if verbose:
1256 output_stream.write("Running %s\n" % filename)
1257 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001258 presubmit_script = gclient_utils.FileRead(filename, 'rU')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001259 results.extend(executer.ExecPresubmitScript(
1260 presubmit_script, filename, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001261
stip@chromium.org5ca27622013-12-18 17:44:58 +00001262
1263 slave_dict = {}
1264 old_style = filter(lambda x: isinstance(x, basestring), results)
1265 new_style = filter(lambda x: isinstance(x, tuple), results)
1266
1267 for result in new_style:
1268 slave_dict.setdefault(result[0], set()).update(result[1])
1269 slaves = list(slave_dict.items())
1270
1271 slaves.extend(set(old_style))
stip@chromium.org68e04192013-11-04 22:14:38 +00001272
thestig@chromium.orgde243452009-10-06 21:02:56 +00001273 if slaves and verbose:
stip@chromium.org5ca27622013-12-18 17:44:58 +00001274 output_stream.write(', '.join((str(x) for x in slaves)))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001275 output_stream.write('\n')
1276 return slaves
1277
1278
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001279def _MergeMasters(masters1, masters2):
1280 """Merges two master maps. Merges also the tests of each builder."""
1281 result = {}
1282 for (master, builders) in itertools.chain(masters1.iteritems(),
1283 masters2.iteritems()):
1284 new_builders = result.setdefault(master, {})
1285 for (builder, tests) in builders.iteritems():
1286 new_builders.setdefault(builder, set([])).update(tests)
1287 return result
1288
1289
1290def DoGetTryMasters(change,
1291 changed_files,
1292 repository_root,
1293 default_presubmit,
1294 project,
1295 verbose,
1296 output_stream):
1297 """Get the list of try masters from the presubmit scripts.
1298
1299 Args:
1300 changed_files: List of modified files.
1301 repository_root: The repository root.
1302 default_presubmit: A default presubmit script to execute in any case.
1303 project: Optional name of a project used in selecting trybots.
1304 verbose: Prints debug info.
1305 output_stream: A stream to write debug output to.
1306
1307 Return:
1308 Map of try masters to map of builders to set of tests.
1309 """
1310 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1311 if not presubmit_files and verbose:
1312 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1313 results = {}
1314 executer = GetTryMastersExecuter()
1315
1316 if default_presubmit:
1317 if verbose:
1318 output_stream.write("Running default presubmit script.\n")
1319 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1320 results = _MergeMasters(results, executer.ExecPresubmitScript(
1321 default_presubmit, fake_path, project, change))
1322 for filename in presubmit_files:
1323 filename = os.path.abspath(filename)
1324 if verbose:
1325 output_stream.write("Running %s\n" % filename)
1326 # Accept CRLF presubmit script.
1327 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1328 results = _MergeMasters(results, executer.ExecPresubmitScript(
1329 presubmit_script, filename, project, change))
1330
1331 # Make sets to lists again for later JSON serialization.
1332 for builders in results.itervalues():
1333 for builder in builders:
1334 builders[builder] = list(builders[builder])
1335
1336 if results and verbose:
1337 output_stream.write('%s\n' % str(results))
1338 return results
1339
1340
rmistry@google.com5626a922015-02-26 14:03:30 +00001341def DoPostUploadExecuter(change,
1342 cl,
1343 repository_root,
1344 verbose,
1345 output_stream):
1346 """Execute the post upload hook.
1347
1348 Args:
1349 change: The Change object.
1350 cl: The Changelist object.
1351 repository_root: The repository root.
1352 verbose: Prints debug info.
1353 output_stream: A stream to write debug output to.
1354 """
1355 presubmit_files = ListRelevantPresubmitFiles(
1356 change.LocalPaths(), repository_root)
1357 if not presubmit_files and verbose:
1358 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1359 results = []
1360 executer = GetPostUploadExecuter()
1361 # The root presubmit file should be executed after the ones in subdirectories.
1362 # i.e. the specific post upload hooks should run before the general ones.
1363 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1364 presubmit_files.reverse()
1365
1366 for filename in presubmit_files:
1367 filename = os.path.abspath(filename)
1368 if verbose:
1369 output_stream.write("Running %s\n" % filename)
1370 # Accept CRLF presubmit script.
1371 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1372 results.extend(executer.ExecPresubmitScript(
1373 presubmit_script, filename, cl, change))
1374 output_stream.write('\n')
1375 if results:
1376 output_stream.write('** Post Upload Hook Messages **\n')
1377 for result in results:
1378 result.handle(output_stream)
1379 output_stream.write('\n')
1380
1381 return results
1382
1383
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001384class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001385 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001386 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001387 """
1388 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001389 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001390 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001391 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001392 gerrit_obj: provides basic Gerrit codereview functionality.
1393 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001394 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001395 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001396 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001397 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001398 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001399 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001400 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001401
1402 def ExecPresubmitScript(self, script_text, presubmit_path):
1403 """Executes a single presubmit script.
1404
1405 Args:
1406 script_text: The text of the presubmit script.
1407 presubmit_path: The path to the presubmit file (this will be reported via
1408 input_api.PresubmitLocalPath()).
1409
1410 Return:
1411 A list of result objects, empty if no problems.
1412 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001413
chase@chromium.org8e416c82009-10-06 04:30:44 +00001414 # Change to the presubmit file's directory to support local imports.
1415 main_path = os.getcwd()
1416 os.chdir(os.path.dirname(presubmit_path))
1417
1418 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001419 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001420 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001421 gerrit_obj=self.gerrit, dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001422 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001423 try:
1424 exec script_text in context
1425 except Exception, e:
1426 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001427
1428 # These function names must change if we make substantial changes to
1429 # the presubmit API that are not backwards compatible.
1430 if self.committing:
1431 function_name = 'CheckChangeOnCommit'
1432 else:
1433 function_name = 'CheckChangeOnUpload'
1434 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001435 context['__args'] = (input_api, OutputApi(self.committing))
tobiasjs2836bcf2016-08-16 04:08:16 -07001436 logging.debug('Running %s in %s', function_name, presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001437 result = eval(function_name + '(*__args)', context)
tobiasjs2836bcf2016-08-16 04:08:16 -07001438 logging.debug('Running %s done.', function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001439 if not (isinstance(result, types.TupleType) or
1440 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001441 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001442 'Presubmit functions must return a tuple or list')
1443 for item in result:
1444 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001445 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001446 'All presubmit results must be of types derived from '
1447 'output_api.PresubmitResult')
1448 else:
1449 result = () # no error since the script doesn't care about current event.
1450
chase@chromium.org8e416c82009-10-06 04:30:44 +00001451 # Return the process to the original working directory.
1452 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001453 return result
1454
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001455
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001456def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001457 committing,
1458 verbose,
1459 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001460 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001461 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001462 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001463 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001464 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001465 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001466 """Runs all presubmit checks that apply to the files in the change.
1467
1468 This finds all PRESUBMIT.py files in directories enclosing the files in the
1469 change (up to the repository root) and calls the relevant entrypoint function
1470 depending on whether the change is being committed or uploaded.
1471
1472 Prints errors, warnings and notifications. Prompts the user for warnings
1473 when needed.
1474
1475 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001476 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001477 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001478 verbose: Prints debug info.
1479 output_stream: A stream to write output from presubmit tests to.
1480 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001481 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001482 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001483 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001484 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001485 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001486
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001487 Warning:
1488 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1489 SHOULD be sys.stdin.
1490
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001491 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001492 A PresubmitOutput object. Use output.should_continue() to figure out
1493 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001494 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001495 old_environ = os.environ
1496 try:
1497 # Make sure python subprocesses won't generate .pyc files.
1498 os.environ = os.environ.copy()
1499 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001500
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001501 output = PresubmitOutput(input_stream, output_stream)
1502 if committing:
1503 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001504 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001505 output.write("Running presubmit upload checks ...\n")
1506 start_time = time.time()
1507 presubmit_files = ListRelevantPresubmitFiles(
1508 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1509 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001510 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001511 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001512 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001513 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001514 if default_presubmit:
1515 if verbose:
1516 output.write("Running default presubmit script.\n")
1517 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1518 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1519 for filename in presubmit_files:
1520 filename = os.path.abspath(filename)
1521 if verbose:
1522 output.write("Running %s\n" % filename)
1523 # Accept CRLF presubmit script.
1524 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1525 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001526
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001527 errors = []
1528 notifications = []
1529 warnings = []
1530 for result in results:
1531 if result.fatal:
1532 errors.append(result)
1533 elif result.should_prompt:
1534 warnings.append(result)
1535 else:
1536 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001537
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001538 output.write('\n')
1539 for name, items in (('Messages', notifications),
1540 ('Warnings', warnings),
1541 ('ERRORS', errors)):
1542 if items:
1543 output.write('** Presubmit %s **\n' % name)
1544 for item in items:
1545 item.handle(output)
1546 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001547
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001548 total_time = time.time() - start_time
1549 if total_time > 1.0:
1550 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001551
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001552 if not errors:
1553 if not warnings:
1554 output.write('Presubmit checks passed.\n')
1555 elif may_prompt:
1556 output.prompt_yes_no('There were presubmit warnings. '
1557 'Are you sure you wish to continue? (y/N): ')
1558 else:
1559 output.fail()
1560
1561 global _ASKED_FOR_FEEDBACK
1562 # Ask for feedback one time out of 5.
1563 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001564 output.write(
1565 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1566 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1567 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001568 _ASKED_FOR_FEEDBACK = True
1569 return output
1570 finally:
1571 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001572
1573
1574def ScanSubDirs(mask, recursive):
1575 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001576 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001577 else:
1578 results = []
1579 for root, dirs, files in os.walk('.'):
1580 if '.svn' in dirs:
1581 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001582 if '.git' in dirs:
1583 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001584 for name in files:
1585 if fnmatch.fnmatch(name, mask):
1586 results.append(os.path.join(root, name))
1587 return results
1588
1589
1590def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001591 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001592 files = []
1593 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001594 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001595 return files
1596
1597
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001598def load_files(options, args):
1599 """Tries to determine the SCM."""
1600 change_scm = scm.determine_scm(options.root)
1601 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001602 if args:
1603 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001604 if change_scm == 'svn':
1605 change_class = SvnChange
1606 if not files:
1607 files = scm.SVN.CaptureStatus([], options.root)
1608 elif change_scm == 'git':
1609 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001610 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001611 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001612 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001613 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001614 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001615 if not files:
1616 return None, None
1617 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001618 return change_class, files
1619
1620
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001621class NonexistantCannedCheckFilter(Exception):
1622 pass
1623
1624
1625@contextlib.contextmanager
1626def canned_check_filter(method_names):
1627 filtered = {}
1628 try:
1629 for method_name in method_names:
1630 if not hasattr(presubmit_canned_checks, method_name):
1631 raise NonexistantCannedCheckFilter(method_name)
1632 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1633 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1634 yield
1635 finally:
1636 for name, method in filtered.iteritems():
1637 setattr(presubmit_canned_checks, name, method)
1638
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001639
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001640def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001641 """Runs an external program, potentially from a child process created by the
1642 multiprocessing module.
1643
1644 multiprocessing needs a top level function with a single argument.
1645 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001646 cmd_data.kwargs['stdout'] = subprocess.PIPE
1647 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1648 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001649 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001650 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001651 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001652 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001653 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001654 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001655 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1656 if code != 0:
1657 return cmd_data.message(
1658 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1659 if cmd_data.info:
1660 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001661
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001662
sbc@chromium.org013731e2015-02-26 18:28:43 +00001663def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001664 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001665 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001666 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001667 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001668 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1669 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001670 parser.add_option("-r", "--recursive", action="store_true",
1671 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001672 parser.add_option("-v", "--verbose", action="count", default=0,
1673 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001674 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001675 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001676 parser.add_option("--description", default='')
1677 parser.add_option("--issue", type='int', default=0)
1678 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001679 parser.add_option("--root", default=os.getcwd(),
1680 help="Search for PRESUBMIT.py up to this directory. "
1681 "If inherit-review-settings-ok is present in this "
1682 "directory, parent directories up to the root file "
1683 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001684 parser.add_option("--upstream",
1685 help="Git only: the base ref or upstream branch against "
1686 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001687 parser.add_option("--default_presubmit")
1688 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001689 parser.add_option("--skip_canned", action='append', default=[],
1690 help="A list of checks to skip which appear in "
1691 "presubmit_canned_checks. Can be provided multiple times "
1692 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001693 parser.add_option("--dry_run", action='store_true',
1694 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001695 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001696 parser.add_option("--gerrit_fetch", action='store_true',
1697 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001698 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1699 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001700 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1701 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001702 # These are for OAuth2 authentication for bots. See also apply_issue.py
1703 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1704 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1705
phajdan.jr@chromium.org2ff30182016-03-23 09:52:51 +00001706 # TODO(phajdan.jr): Update callers and remove obsolete --trybot-json .
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001707 parser.add_option("--trybot-json",
1708 help="Output trybot information to the file specified.")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001709 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001710 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001711 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001712
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001713 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001714 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001715 elif options.verbose:
1716 logging.basicConfig(level=logging.INFO)
1717 else:
1718 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001719
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001720 if (any((options.rietveld_url, options.rietveld_email_file,
1721 options.rietveld_fetch, options.rietveld_private_key_file))
1722 and any((options.gerrit_url, options.gerrit_fetch))):
1723 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1724 'allowed')
1725
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001726 if options.rietveld_email and options.rietveld_email_file:
1727 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1728 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001729 if options.rietveld_email_file:
1730 with open(options.rietveld_email_file, "rb") as f:
1731 options.rietveld_email = f.read().strip()
1732
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001733 change_class, files = load_files(options, args)
1734 if not change_class:
1735 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001736 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001737
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001738 rietveld_obj, gerrit_obj = None, None
1739
maruel@chromium.org239f4112011-06-03 20:08:23 +00001740 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001741 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001742 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001743 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1744 options.rietveld_url,
1745 options.rietveld_email,
1746 options.rietveld_private_key_file)
1747 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001748 rietveld_obj = rietveld.CachingRietveld(
1749 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001750 auth_config,
1751 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001752 if options.rietveld_fetch:
1753 assert options.issue
1754 props = rietveld_obj.get_issue_properties(options.issue, False)
1755 options.author = props['owner_email']
1756 options.description = props['description']
1757 logging.info('Got author: "%s"', options.author)
1758 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001759
1760 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001761 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001762 rietveld_obj = None
1763 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1764 options.author = gerrit_obj.GetChangeOwner(options.issue)
1765 options.description = gerrit_obj.GetChangeDescription(options.issue,
1766 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001767 logging.info('Got author: "%s"', options.author)
1768 logging.info('Got description: """\n%s\n"""', options.description)
1769
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001770 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001771 with canned_check_filter(options.skip_canned):
1772 results = DoPresubmitChecks(
1773 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001774 options.description,
1775 options.root,
1776 files,
1777 options.issue,
1778 options.patchset,
1779 options.author,
1780 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001781 options.commit,
1782 options.verbose,
1783 sys.stdout,
1784 sys.stdin,
1785 options.default_presubmit,
1786 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001787 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001788 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001789 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001790 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001791 except NonexistantCannedCheckFilter, e:
1792 print >> sys.stderr, (
1793 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1794 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001795 except PresubmitFailure, e:
1796 print >> sys.stderr, e
1797 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1798 print >> sys.stderr, 'If all fails, contact maruel@'
1799 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001800
1801
1802if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001803 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001804 try:
1805 sys.exit(main())
1806 except KeyboardInterrupt:
1807 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001808 sys.exit(2)