blob: 2922b4e32938c873da53decfc4e646e9b8d73174 [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
1012 def _GetChangeLists(self):
1013 """Get all change lists."""
1014 if self._changelists == None:
1015 previous_cwd = os.getcwd()
1016 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001017 # Need to import here to avoid circular dependency.
1018 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +00001019 self._changelists = gcl.GetModifiedFiles()
1020 os.chdir(previous_cwd)
1021 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001022
1023 def GetAllModifiedFiles(self):
1024 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +00001025 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001026 all_modified_files = []
1027 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +00001028 all_modified_files.extend(
1029 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001030 return all_modified_files
1031
1032 def GetModifiedFiles(self):
1033 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +00001034 changelists = self._GetChangeLists()
1035 return [os.path.join(self.RepositoryRoot(), f[1])
1036 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001037
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001038 def AllFiles(self, root=None):
1039 """List all files under source control in the repo."""
1040 root = root or self.RepositoryRoot()
1041 return subprocess.check_output(
1042 ['svn', 'ls', '-R', '.'], cwd=root).splitlines()
1043
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001044
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001045class GitChange(Change):
1046 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001047 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001048
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001049 def AllFiles(self, root=None):
1050 """List all files under source control in the repo."""
1051 root = root or self.RepositoryRoot()
1052 return subprocess.check_output(
1053 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
1054
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001055
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001056def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001057 """Finds all presubmit files that apply to a given set of source files.
1058
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001059 If inherit-review-settings-ok is present right under root, looks for
1060 PRESUBMIT.py in directories enclosing root.
1061
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001062 Args:
1063 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001064 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001065
1066 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001067 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001068 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001069 files = [normpath(os.path.join(root, f)) for f in files]
1070
1071 # List all the individual directories containing files.
1072 directories = set([os.path.dirname(f) for f in files])
1073
1074 # Ignore root if inherit-review-settings-ok is present.
1075 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1076 root = None
1077
1078 # Collect all unique directories that may contain PRESUBMIT.py.
1079 candidates = set()
1080 for directory in directories:
1081 while True:
1082 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001083 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001084 candidates.add(directory)
1085 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001086 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001087 parent_dir = os.path.dirname(directory)
1088 if parent_dir == directory:
1089 # We hit the system root directory.
1090 break
1091 directory = parent_dir
1092
1093 # Look for PRESUBMIT.py in all candidate directories.
1094 results = []
1095 for directory in sorted(list(candidates)):
tobiasjs2836bcf2016-08-16 04:08:16 -07001096 for f in os.listdir(directory):
1097 p = os.path.join(directory, f)
1098 if os.path.isfile(p) and re.match(
1099 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1100 results.append(p)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001101
tobiasjs2836bcf2016-08-16 04:08:16 -07001102 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001103 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001104
1105
thestig@chromium.orgde243452009-10-06 21:02:56 +00001106class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001107 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001108 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +00001109 """Executes GetPreferredTrySlaves() from a single presubmit script.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001110
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001111 This will soon be deprecated and replaced by GetPreferredTryMasters().
thestig@chromium.orgde243452009-10-06 21:02:56 +00001112
1113 Args:
1114 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +00001115 presubmit_path: Project script to run.
1116 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001117
1118 Return:
1119 A list of try slaves.
1120 """
1121 context = {}
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001122 main_path = os.getcwd()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001123 try:
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001124 os.chdir(os.path.dirname(presubmit_path))
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001125 exec script_text in context
1126 except Exception, e:
1127 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001128 finally:
1129 os.chdir(main_path)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001130
1131 function_name = 'GetPreferredTrySlaves'
1132 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001133 get_preferred_try_slaves = context[function_name]
1134 function_info = inspect.getargspec(get_preferred_try_slaves)
1135 if len(function_info[0]) == 1:
1136 result = get_preferred_try_slaves(project)
1137 elif len(function_info[0]) == 2:
1138 result = get_preferred_try_slaves(project, change)
1139 else:
1140 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +00001141 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001142 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +00001143 'Presubmit functions must return a list, got a %s instead: %s' %
1144 (type(result), str(result)))
1145 for item in result:
stip@chromium.org68e04192013-11-04 22:14:38 +00001146 if isinstance(item, basestring):
1147 # Old-style ['bot'] format.
1148 botname = item
1149 elif isinstance(item, tuple):
1150 # New-style [('bot', set(['tests']))] format.
1151 botname = item[0]
1152 else:
1153 raise PresubmitFailure('PRESUBMIT.py returned invalid tryslave/test'
1154 ' format.')
1155
1156 if botname != botname.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001157 raise PresubmitFailure(
1158 'Try slave names cannot start/end with whitespace')
stip@chromium.org68e04192013-11-04 22:14:38 +00001159 if ',' in botname:
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +00001160 raise PresubmitFailure(
stip@chromium.org68e04192013-11-04 22:14:38 +00001161 'Do not use \',\' separated builder or test names: %s' % botname)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001162 else:
1163 result = []
stip@chromium.org5ca27622013-12-18 17:44:58 +00001164
1165 def valid_oldstyle(result):
1166 return all(isinstance(i, basestring) for i in result)
1167
1168 def valid_newstyle(result):
1169 return (all(isinstance(i, tuple) for i in result) and
1170 all(len(i) == 2 for i in result) and
1171 all(isinstance(i[0], basestring) for i in result) and
1172 all(isinstance(i[1], set) for i in result)
1173 )
1174
1175 # Ensure it's either all old-style or all new-style.
1176 if not valid_oldstyle(result) and not valid_newstyle(result):
1177 raise PresubmitFailure(
1178 'PRESUBMIT.py returned invalid trybot specification!')
1179
thestig@chromium.orgde243452009-10-06 21:02:56 +00001180 return result
1181
1182
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001183class GetTryMastersExecuter(object):
1184 @staticmethod
1185 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1186 """Executes GetPreferredTryMasters() from a single presubmit script.
1187
1188 Args:
1189 script_text: The text of the presubmit script.
1190 presubmit_path: Project script to run.
1191 project: Project name to pass to presubmit script for bot selection.
1192
1193 Return:
1194 A map of try masters to map of builders to set of tests.
1195 """
1196 context = {}
1197 try:
1198 exec script_text in context
1199 except Exception, e:
1200 raise PresubmitFailure('"%s" had an exception.\n%s'
1201 % (presubmit_path, e))
1202
1203 function_name = 'GetPreferredTryMasters'
1204 if function_name not in context:
1205 return {}
1206 get_preferred_try_masters = context[function_name]
1207 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1208 raise PresubmitFailure(
1209 'Expected function "GetPreferredTryMasters" to take two arguments.')
1210 return get_preferred_try_masters(project, change)
1211
1212
rmistry@google.com5626a922015-02-26 14:03:30 +00001213class GetPostUploadExecuter(object):
1214 @staticmethod
1215 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1216 """Executes PostUploadHook() from a single presubmit script.
1217
1218 Args:
1219 script_text: The text of the presubmit script.
1220 presubmit_path: Project script to run.
1221 cl: The Changelist object.
1222 change: The Change object.
1223
1224 Return:
1225 A list of results objects.
1226 """
1227 context = {}
1228 try:
1229 exec script_text in context
1230 except Exception, e:
1231 raise PresubmitFailure('"%s" had an exception.\n%s'
1232 % (presubmit_path, e))
1233
1234 function_name = 'PostUploadHook'
1235 if function_name not in context:
1236 return {}
1237 post_upload_hook = context[function_name]
1238 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1239 raise PresubmitFailure(
1240 'Expected function "PostUploadHook" to take three arguments.')
1241 return post_upload_hook(cl, change, OutputApi(False))
1242
1243
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001244def DoGetTrySlaves(change,
1245 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001246 repository_root,
1247 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +00001248 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001249 verbose,
1250 output_stream):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001251 """Get the list of try servers from the presubmit scripts (deprecated).
thestig@chromium.orgde243452009-10-06 21:02:56 +00001252
1253 Args:
1254 changed_files: List of modified files.
1255 repository_root: The repository root.
1256 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +00001257 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001258 verbose: Prints debug info.
1259 output_stream: A stream to write debug output to.
1260
1261 Return:
1262 List of try slaves
1263 """
1264 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1265 if not presubmit_files and verbose:
mdempsky@chromium.orgd59e7612014-03-05 19:55:56 +00001266 output_stream.write("Warning, no PRESUBMIT.py found.\n")
thestig@chromium.orgde243452009-10-06 21:02:56 +00001267 results = []
1268 executer = GetTrySlavesExecuter()
stip@chromium.org5ca27622013-12-18 17:44:58 +00001269
thestig@chromium.orgde243452009-10-06 21:02:56 +00001270 if default_presubmit:
1271 if verbose:
1272 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001273 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001274 results.extend(executer.ExecPresubmitScript(
1275 default_presubmit, fake_path, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001276 for filename in presubmit_files:
1277 filename = os.path.abspath(filename)
1278 if verbose:
1279 output_stream.write("Running %s\n" % filename)
1280 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001281 presubmit_script = gclient_utils.FileRead(filename, 'rU')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001282 results.extend(executer.ExecPresubmitScript(
1283 presubmit_script, filename, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001284
stip@chromium.org5ca27622013-12-18 17:44:58 +00001285
1286 slave_dict = {}
1287 old_style = filter(lambda x: isinstance(x, basestring), results)
1288 new_style = filter(lambda x: isinstance(x, tuple), results)
1289
1290 for result in new_style:
1291 slave_dict.setdefault(result[0], set()).update(result[1])
1292 slaves = list(slave_dict.items())
1293
1294 slaves.extend(set(old_style))
stip@chromium.org68e04192013-11-04 22:14:38 +00001295
thestig@chromium.orgde243452009-10-06 21:02:56 +00001296 if slaves and verbose:
stip@chromium.org5ca27622013-12-18 17:44:58 +00001297 output_stream.write(', '.join((str(x) for x in slaves)))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001298 output_stream.write('\n')
1299 return slaves
1300
1301
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001302def _MergeMasters(masters1, masters2):
1303 """Merges two master maps. Merges also the tests of each builder."""
1304 result = {}
1305 for (master, builders) in itertools.chain(masters1.iteritems(),
1306 masters2.iteritems()):
1307 new_builders = result.setdefault(master, {})
1308 for (builder, tests) in builders.iteritems():
1309 new_builders.setdefault(builder, set([])).update(tests)
1310 return result
1311
1312
1313def DoGetTryMasters(change,
1314 changed_files,
1315 repository_root,
1316 default_presubmit,
1317 project,
1318 verbose,
1319 output_stream):
1320 """Get the list of try masters from the presubmit scripts.
1321
1322 Args:
1323 changed_files: List of modified files.
1324 repository_root: The repository root.
1325 default_presubmit: A default presubmit script to execute in any case.
1326 project: Optional name of a project used in selecting trybots.
1327 verbose: Prints debug info.
1328 output_stream: A stream to write debug output to.
1329
1330 Return:
1331 Map of try masters to map of builders to set of tests.
1332 """
1333 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1334 if not presubmit_files and verbose:
1335 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1336 results = {}
1337 executer = GetTryMastersExecuter()
1338
1339 if default_presubmit:
1340 if verbose:
1341 output_stream.write("Running default presubmit script.\n")
1342 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1343 results = _MergeMasters(results, executer.ExecPresubmitScript(
1344 default_presubmit, fake_path, project, change))
1345 for filename in presubmit_files:
1346 filename = os.path.abspath(filename)
1347 if verbose:
1348 output_stream.write("Running %s\n" % filename)
1349 # Accept CRLF presubmit script.
1350 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1351 results = _MergeMasters(results, executer.ExecPresubmitScript(
1352 presubmit_script, filename, project, change))
1353
1354 # Make sets to lists again for later JSON serialization.
1355 for builders in results.itervalues():
1356 for builder in builders:
1357 builders[builder] = list(builders[builder])
1358
1359 if results and verbose:
1360 output_stream.write('%s\n' % str(results))
1361 return results
1362
1363
rmistry@google.com5626a922015-02-26 14:03:30 +00001364def DoPostUploadExecuter(change,
1365 cl,
1366 repository_root,
1367 verbose,
1368 output_stream):
1369 """Execute the post upload hook.
1370
1371 Args:
1372 change: The Change object.
1373 cl: The Changelist object.
1374 repository_root: The repository root.
1375 verbose: Prints debug info.
1376 output_stream: A stream to write debug output to.
1377 """
1378 presubmit_files = ListRelevantPresubmitFiles(
1379 change.LocalPaths(), repository_root)
1380 if not presubmit_files and verbose:
1381 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1382 results = []
1383 executer = GetPostUploadExecuter()
1384 # The root presubmit file should be executed after the ones in subdirectories.
1385 # i.e. the specific post upload hooks should run before the general ones.
1386 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1387 presubmit_files.reverse()
1388
1389 for filename in presubmit_files:
1390 filename = os.path.abspath(filename)
1391 if verbose:
1392 output_stream.write("Running %s\n" % filename)
1393 # Accept CRLF presubmit script.
1394 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1395 results.extend(executer.ExecPresubmitScript(
1396 presubmit_script, filename, cl, change))
1397 output_stream.write('\n')
1398 if results:
1399 output_stream.write('** Post Upload Hook Messages **\n')
1400 for result in results:
1401 result.handle(output_stream)
1402 output_stream.write('\n')
1403
1404 return results
1405
1406
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001407class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001408 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001409 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001410 """
1411 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001412 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001413 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001414 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001415 gerrit_obj: provides basic Gerrit codereview functionality.
1416 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001417 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001418 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001419 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001420 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001421 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001422 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001423 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001424
1425 def ExecPresubmitScript(self, script_text, presubmit_path):
1426 """Executes a single presubmit script.
1427
1428 Args:
1429 script_text: The text of the presubmit script.
1430 presubmit_path: The path to the presubmit file (this will be reported via
1431 input_api.PresubmitLocalPath()).
1432
1433 Return:
1434 A list of result objects, empty if no problems.
1435 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001436
chase@chromium.org8e416c82009-10-06 04:30:44 +00001437 # Change to the presubmit file's directory to support local imports.
1438 main_path = os.getcwd()
1439 os.chdir(os.path.dirname(presubmit_path))
1440
1441 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001442 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001443 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001444 gerrit_obj=self.gerrit, dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001445 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001446 try:
1447 exec script_text in context
1448 except Exception, e:
1449 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001450
1451 # These function names must change if we make substantial changes to
1452 # the presubmit API that are not backwards compatible.
1453 if self.committing:
1454 function_name = 'CheckChangeOnCommit'
1455 else:
1456 function_name = 'CheckChangeOnUpload'
1457 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001458 context['__args'] = (input_api, OutputApi(self.committing))
tobiasjs2836bcf2016-08-16 04:08:16 -07001459 logging.debug('Running %s in %s', function_name, presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001460 result = eval(function_name + '(*__args)', context)
tobiasjs2836bcf2016-08-16 04:08:16 -07001461 logging.debug('Running %s done.', function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001462 if not (isinstance(result, types.TupleType) or
1463 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001464 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001465 'Presubmit functions must return a tuple or list')
1466 for item in result:
1467 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001468 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001469 'All presubmit results must be of types derived from '
1470 'output_api.PresubmitResult')
1471 else:
1472 result = () # no error since the script doesn't care about current event.
1473
chase@chromium.org8e416c82009-10-06 04:30:44 +00001474 # Return the process to the original working directory.
1475 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001476 return result
1477
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001478
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001479def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001480 committing,
1481 verbose,
1482 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001483 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001484 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001485 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001486 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001487 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001488 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001489 """Runs all presubmit checks that apply to the files in the change.
1490
1491 This finds all PRESUBMIT.py files in directories enclosing the files in the
1492 change (up to the repository root) and calls the relevant entrypoint function
1493 depending on whether the change is being committed or uploaded.
1494
1495 Prints errors, warnings and notifications. Prompts the user for warnings
1496 when needed.
1497
1498 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001499 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001500 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1501 verbose: Prints debug info.
1502 output_stream: A stream to write output from presubmit tests to.
1503 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001504 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001505 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001506 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001507 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001508 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001509
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001510 Warning:
1511 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1512 SHOULD be sys.stdin.
1513
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001514 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001515 A PresubmitOutput object. Use output.should_continue() to figure out
1516 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001517 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001518 old_environ = os.environ
1519 try:
1520 # Make sure python subprocesses won't generate .pyc files.
1521 os.environ = os.environ.copy()
1522 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001523
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001524 output = PresubmitOutput(input_stream, output_stream)
1525 if committing:
1526 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001527 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001528 output.write("Running presubmit upload checks ...\n")
1529 start_time = time.time()
1530 presubmit_files = ListRelevantPresubmitFiles(
1531 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1532 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001533 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001534 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001535 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001536 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001537 if default_presubmit:
1538 if verbose:
1539 output.write("Running default presubmit script.\n")
1540 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1541 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1542 for filename in presubmit_files:
1543 filename = os.path.abspath(filename)
1544 if verbose:
1545 output.write("Running %s\n" % filename)
1546 # Accept CRLF presubmit script.
1547 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1548 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001549
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001550 errors = []
1551 notifications = []
1552 warnings = []
1553 for result in results:
1554 if result.fatal:
1555 errors.append(result)
1556 elif result.should_prompt:
1557 warnings.append(result)
1558 else:
1559 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001560
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001561 output.write('\n')
1562 for name, items in (('Messages', notifications),
1563 ('Warnings', warnings),
1564 ('ERRORS', errors)):
1565 if items:
1566 output.write('** Presubmit %s **\n' % name)
1567 for item in items:
1568 item.handle(output)
1569 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001570
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001571 total_time = time.time() - start_time
1572 if total_time > 1.0:
1573 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001574
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001575 if not errors:
1576 if not warnings:
1577 output.write('Presubmit checks passed.\n')
1578 elif may_prompt:
1579 output.prompt_yes_no('There were presubmit warnings. '
1580 'Are you sure you wish to continue? (y/N): ')
1581 else:
1582 output.fail()
1583
1584 global _ASKED_FOR_FEEDBACK
1585 # Ask for feedback one time out of 5.
1586 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001587 output.write(
1588 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1589 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1590 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001591 _ASKED_FOR_FEEDBACK = True
1592 return output
1593 finally:
1594 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001595
1596
1597def ScanSubDirs(mask, recursive):
1598 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001599 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001600 else:
1601 results = []
1602 for root, dirs, files in os.walk('.'):
1603 if '.svn' in dirs:
1604 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001605 if '.git' in dirs:
1606 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001607 for name in files:
1608 if fnmatch.fnmatch(name, mask):
1609 results.append(os.path.join(root, name))
1610 return results
1611
1612
1613def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001614 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001615 files = []
1616 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001617 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001618 return files
1619
1620
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001621def load_files(options, args):
1622 """Tries to determine the SCM."""
1623 change_scm = scm.determine_scm(options.root)
1624 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001625 if args:
1626 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001627 if change_scm == 'svn':
1628 change_class = SvnChange
1629 if not files:
1630 files = scm.SVN.CaptureStatus([], options.root)
1631 elif change_scm == 'git':
1632 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001633 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001634 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001635 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001636 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001637 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001638 if not files:
1639 return None, None
1640 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001641 return change_class, files
1642
1643
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001644class NonexistantCannedCheckFilter(Exception):
1645 pass
1646
1647
1648@contextlib.contextmanager
1649def canned_check_filter(method_names):
1650 filtered = {}
1651 try:
1652 for method_name in method_names:
1653 if not hasattr(presubmit_canned_checks, method_name):
1654 raise NonexistantCannedCheckFilter(method_name)
1655 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1656 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1657 yield
1658 finally:
1659 for name, method in filtered.iteritems():
1660 setattr(presubmit_canned_checks, name, method)
1661
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001662
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001663def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001664 """Runs an external program, potentially from a child process created by the
1665 multiprocessing module.
1666
1667 multiprocessing needs a top level function with a single argument.
1668 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001669 cmd_data.kwargs['stdout'] = subprocess.PIPE
1670 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1671 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001672 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001673 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001674 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001675 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001676 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001677 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001678 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1679 if code != 0:
1680 return cmd_data.message(
1681 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1682 if cmd_data.info:
1683 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001684
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001685
sbc@chromium.org013731e2015-02-26 18:28:43 +00001686def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001687 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001688 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001689 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001690 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001691 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1692 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001693 parser.add_option("-r", "--recursive", action="store_true",
1694 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001695 parser.add_option("-v", "--verbose", action="count", default=0,
1696 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001697 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001698 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001699 parser.add_option("--description", default='')
1700 parser.add_option("--issue", type='int', default=0)
1701 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001702 parser.add_option("--root", default=os.getcwd(),
1703 help="Search for PRESUBMIT.py up to this directory. "
1704 "If inherit-review-settings-ok is present in this "
1705 "directory, parent directories up to the root file "
1706 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001707 parser.add_option("--upstream",
1708 help="Git only: the base ref or upstream branch against "
1709 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001710 parser.add_option("--default_presubmit")
1711 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001712 parser.add_option("--skip_canned", action='append', default=[],
1713 help="A list of checks to skip which appear in "
1714 "presubmit_canned_checks. Can be provided multiple times "
1715 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001716 parser.add_option("--dry_run", action='store_true',
1717 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001718 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001719 parser.add_option("--gerrit_fetch", action='store_true',
1720 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001721 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1722 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001723 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1724 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001725 # These are for OAuth2 authentication for bots. See also apply_issue.py
1726 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1727 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1728
phajdan.jr@chromium.org2ff30182016-03-23 09:52:51 +00001729 # TODO(phajdan.jr): Update callers and remove obsolete --trybot-json .
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001730 parser.add_option("--trybot-json",
1731 help="Output trybot information to the file specified.")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001732 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001733 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001734 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001735
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001736 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001737 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001738 elif options.verbose:
1739 logging.basicConfig(level=logging.INFO)
1740 else:
1741 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001742
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001743 if (any((options.rietveld_url, options.rietveld_email_file,
1744 options.rietveld_fetch, options.rietveld_private_key_file))
1745 and any((options.gerrit_url, options.gerrit_fetch))):
1746 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1747 'allowed')
1748
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001749 if options.rietveld_email and options.rietveld_email_file:
1750 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1751 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001752 if options.rietveld_email_file:
1753 with open(options.rietveld_email_file, "rb") as f:
1754 options.rietveld_email = f.read().strip()
1755
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001756 change_class, files = load_files(options, args)
1757 if not change_class:
1758 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001759 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001760
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001761 rietveld_obj, gerrit_obj = None, None
1762
maruel@chromium.org239f4112011-06-03 20:08:23 +00001763 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001764 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001765 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001766 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1767 options.rietveld_url,
1768 options.rietveld_email,
1769 options.rietveld_private_key_file)
1770 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001771 rietveld_obj = rietveld.CachingRietveld(
1772 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001773 auth_config,
1774 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001775 if options.rietveld_fetch:
1776 assert options.issue
1777 props = rietveld_obj.get_issue_properties(options.issue, False)
1778 options.author = props['owner_email']
1779 options.description = props['description']
1780 logging.info('Got author: "%s"', options.author)
1781 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001782
1783 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001784 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001785 rietveld_obj = None
1786 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1787 options.author = gerrit_obj.GetChangeOwner(options.issue)
1788 options.description = gerrit_obj.GetChangeDescription(options.issue,
1789 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001790 logging.info('Got author: "%s"', options.author)
1791 logging.info('Got description: """\n%s\n"""', options.description)
1792
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001793 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001794 with canned_check_filter(options.skip_canned):
1795 results = DoPresubmitChecks(
1796 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001797 options.description,
1798 options.root,
1799 files,
1800 options.issue,
1801 options.patchset,
1802 options.author,
1803 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001804 options.commit,
1805 options.verbose,
1806 sys.stdout,
1807 sys.stdin,
1808 options.default_presubmit,
1809 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001810 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001811 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001812 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001813 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001814 except NonexistantCannedCheckFilter, e:
1815 print >> sys.stderr, (
1816 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1817 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001818 except PresubmitFailure, e:
1819 print >> sys.stderr, e
1820 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1821 print >> sys.stderr, 'If all fails, contact maruel@'
1822 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001823
1824
1825if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001826 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001827 try:
1828 sys.exit(main())
1829 except KeyboardInterrupt:
1830 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001831 sys.exit(2)