blob: 807da4a05d1458751207f96cc02f8ff21a35cd10 [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):
497 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
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000649 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)):
1096 p = os.path.join(directory, 'PRESUBMIT.py')
1097 if os.path.isfile(p):
1098 results.append(p)
1099
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001100 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001101 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001102
1103
thestig@chromium.orgde243452009-10-06 21:02:56 +00001104class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001105 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001106 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +00001107 """Executes GetPreferredTrySlaves() from a single presubmit script.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001108
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001109 This will soon be deprecated and replaced by GetPreferredTryMasters().
thestig@chromium.orgde243452009-10-06 21:02:56 +00001110
1111 Args:
1112 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +00001113 presubmit_path: Project script to run.
1114 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001115
1116 Return:
1117 A list of try slaves.
1118 """
1119 context = {}
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001120 main_path = os.getcwd()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001121 try:
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001122 os.chdir(os.path.dirname(presubmit_path))
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001123 exec script_text in context
1124 except Exception, e:
1125 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001126 finally:
1127 os.chdir(main_path)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001128
1129 function_name = 'GetPreferredTrySlaves'
1130 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001131 get_preferred_try_slaves = context[function_name]
1132 function_info = inspect.getargspec(get_preferred_try_slaves)
1133 if len(function_info[0]) == 1:
1134 result = get_preferred_try_slaves(project)
1135 elif len(function_info[0]) == 2:
1136 result = get_preferred_try_slaves(project, change)
1137 else:
1138 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +00001139 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001140 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +00001141 'Presubmit functions must return a list, got a %s instead: %s' %
1142 (type(result), str(result)))
1143 for item in result:
stip@chromium.org68e04192013-11-04 22:14:38 +00001144 if isinstance(item, basestring):
1145 # Old-style ['bot'] format.
1146 botname = item
1147 elif isinstance(item, tuple):
1148 # New-style [('bot', set(['tests']))] format.
1149 botname = item[0]
1150 else:
1151 raise PresubmitFailure('PRESUBMIT.py returned invalid tryslave/test'
1152 ' format.')
1153
1154 if botname != botname.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001155 raise PresubmitFailure(
1156 'Try slave names cannot start/end with whitespace')
stip@chromium.org68e04192013-11-04 22:14:38 +00001157 if ',' in botname:
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +00001158 raise PresubmitFailure(
stip@chromium.org68e04192013-11-04 22:14:38 +00001159 'Do not use \',\' separated builder or test names: %s' % botname)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001160 else:
1161 result = []
stip@chromium.org5ca27622013-12-18 17:44:58 +00001162
1163 def valid_oldstyle(result):
1164 return all(isinstance(i, basestring) for i in result)
1165
1166 def valid_newstyle(result):
1167 return (all(isinstance(i, tuple) for i in result) and
1168 all(len(i) == 2 for i in result) and
1169 all(isinstance(i[0], basestring) for i in result) and
1170 all(isinstance(i[1], set) for i in result)
1171 )
1172
1173 # Ensure it's either all old-style or all new-style.
1174 if not valid_oldstyle(result) and not valid_newstyle(result):
1175 raise PresubmitFailure(
1176 'PRESUBMIT.py returned invalid trybot specification!')
1177
thestig@chromium.orgde243452009-10-06 21:02:56 +00001178 return result
1179
1180
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001181class GetTryMastersExecuter(object):
1182 @staticmethod
1183 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1184 """Executes GetPreferredTryMasters() from a single presubmit script.
1185
1186 Args:
1187 script_text: The text of the presubmit script.
1188 presubmit_path: Project script to run.
1189 project: Project name to pass to presubmit script for bot selection.
1190
1191 Return:
1192 A map of try masters to map of builders to set of tests.
1193 """
1194 context = {}
1195 try:
1196 exec script_text in context
1197 except Exception, e:
1198 raise PresubmitFailure('"%s" had an exception.\n%s'
1199 % (presubmit_path, e))
1200
1201 function_name = 'GetPreferredTryMasters'
1202 if function_name not in context:
1203 return {}
1204 get_preferred_try_masters = context[function_name]
1205 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1206 raise PresubmitFailure(
1207 'Expected function "GetPreferredTryMasters" to take two arguments.')
1208 return get_preferred_try_masters(project, change)
1209
1210
rmistry@google.com5626a922015-02-26 14:03:30 +00001211class GetPostUploadExecuter(object):
1212 @staticmethod
1213 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1214 """Executes PostUploadHook() from a single presubmit script.
1215
1216 Args:
1217 script_text: The text of the presubmit script.
1218 presubmit_path: Project script to run.
1219 cl: The Changelist object.
1220 change: The Change object.
1221
1222 Return:
1223 A list of results objects.
1224 """
1225 context = {}
1226 try:
1227 exec script_text in context
1228 except Exception, e:
1229 raise PresubmitFailure('"%s" had an exception.\n%s'
1230 % (presubmit_path, e))
1231
1232 function_name = 'PostUploadHook'
1233 if function_name not in context:
1234 return {}
1235 post_upload_hook = context[function_name]
1236 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1237 raise PresubmitFailure(
1238 'Expected function "PostUploadHook" to take three arguments.')
1239 return post_upload_hook(cl, change, OutputApi(False))
1240
1241
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001242def DoGetTrySlaves(change,
1243 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001244 repository_root,
1245 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +00001246 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001247 verbose,
1248 output_stream):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001249 """Get the list of try servers from the presubmit scripts (deprecated).
thestig@chromium.orgde243452009-10-06 21:02:56 +00001250
1251 Args:
1252 changed_files: List of modified files.
1253 repository_root: The repository root.
1254 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +00001255 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001256 verbose: Prints debug info.
1257 output_stream: A stream to write debug output to.
1258
1259 Return:
1260 List of try slaves
1261 """
1262 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1263 if not presubmit_files and verbose:
mdempsky@chromium.orgd59e7612014-03-05 19:55:56 +00001264 output_stream.write("Warning, no PRESUBMIT.py found.\n")
thestig@chromium.orgde243452009-10-06 21:02:56 +00001265 results = []
1266 executer = GetTrySlavesExecuter()
stip@chromium.org5ca27622013-12-18 17:44:58 +00001267
thestig@chromium.orgde243452009-10-06 21:02:56 +00001268 if default_presubmit:
1269 if verbose:
1270 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001271 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001272 results.extend(executer.ExecPresubmitScript(
1273 default_presubmit, fake_path, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001274 for filename in presubmit_files:
1275 filename = os.path.abspath(filename)
1276 if verbose:
1277 output_stream.write("Running %s\n" % filename)
1278 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001279 presubmit_script = gclient_utils.FileRead(filename, 'rU')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001280 results.extend(executer.ExecPresubmitScript(
1281 presubmit_script, filename, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001282
stip@chromium.org5ca27622013-12-18 17:44:58 +00001283
1284 slave_dict = {}
1285 old_style = filter(lambda x: isinstance(x, basestring), results)
1286 new_style = filter(lambda x: isinstance(x, tuple), results)
1287
1288 for result in new_style:
1289 slave_dict.setdefault(result[0], set()).update(result[1])
1290 slaves = list(slave_dict.items())
1291
1292 slaves.extend(set(old_style))
stip@chromium.org68e04192013-11-04 22:14:38 +00001293
thestig@chromium.orgde243452009-10-06 21:02:56 +00001294 if slaves and verbose:
stip@chromium.org5ca27622013-12-18 17:44:58 +00001295 output_stream.write(', '.join((str(x) for x in slaves)))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001296 output_stream.write('\n')
1297 return slaves
1298
1299
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001300def _MergeMasters(masters1, masters2):
1301 """Merges two master maps. Merges also the tests of each builder."""
1302 result = {}
1303 for (master, builders) in itertools.chain(masters1.iteritems(),
1304 masters2.iteritems()):
1305 new_builders = result.setdefault(master, {})
1306 for (builder, tests) in builders.iteritems():
1307 new_builders.setdefault(builder, set([])).update(tests)
1308 return result
1309
1310
1311def DoGetTryMasters(change,
1312 changed_files,
1313 repository_root,
1314 default_presubmit,
1315 project,
1316 verbose,
1317 output_stream):
1318 """Get the list of try masters from the presubmit scripts.
1319
1320 Args:
1321 changed_files: List of modified files.
1322 repository_root: The repository root.
1323 default_presubmit: A default presubmit script to execute in any case.
1324 project: Optional name of a project used in selecting trybots.
1325 verbose: Prints debug info.
1326 output_stream: A stream to write debug output to.
1327
1328 Return:
1329 Map of try masters to map of builders to set of tests.
1330 """
1331 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1332 if not presubmit_files and verbose:
1333 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1334 results = {}
1335 executer = GetTryMastersExecuter()
1336
1337 if default_presubmit:
1338 if verbose:
1339 output_stream.write("Running default presubmit script.\n")
1340 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1341 results = _MergeMasters(results, executer.ExecPresubmitScript(
1342 default_presubmit, fake_path, project, change))
1343 for filename in presubmit_files:
1344 filename = os.path.abspath(filename)
1345 if verbose:
1346 output_stream.write("Running %s\n" % filename)
1347 # Accept CRLF presubmit script.
1348 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1349 results = _MergeMasters(results, executer.ExecPresubmitScript(
1350 presubmit_script, filename, project, change))
1351
1352 # Make sets to lists again for later JSON serialization.
1353 for builders in results.itervalues():
1354 for builder in builders:
1355 builders[builder] = list(builders[builder])
1356
1357 if results and verbose:
1358 output_stream.write('%s\n' % str(results))
1359 return results
1360
1361
rmistry@google.com5626a922015-02-26 14:03:30 +00001362def DoPostUploadExecuter(change,
1363 cl,
1364 repository_root,
1365 verbose,
1366 output_stream):
1367 """Execute the post upload hook.
1368
1369 Args:
1370 change: The Change object.
1371 cl: The Changelist object.
1372 repository_root: The repository root.
1373 verbose: Prints debug info.
1374 output_stream: A stream to write debug output to.
1375 """
1376 presubmit_files = ListRelevantPresubmitFiles(
1377 change.LocalPaths(), repository_root)
1378 if not presubmit_files and verbose:
1379 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1380 results = []
1381 executer = GetPostUploadExecuter()
1382 # The root presubmit file should be executed after the ones in subdirectories.
1383 # i.e. the specific post upload hooks should run before the general ones.
1384 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1385 presubmit_files.reverse()
1386
1387 for filename in presubmit_files:
1388 filename = os.path.abspath(filename)
1389 if verbose:
1390 output_stream.write("Running %s\n" % filename)
1391 # Accept CRLF presubmit script.
1392 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1393 results.extend(executer.ExecPresubmitScript(
1394 presubmit_script, filename, cl, change))
1395 output_stream.write('\n')
1396 if results:
1397 output_stream.write('** Post Upload Hook Messages **\n')
1398 for result in results:
1399 result.handle(output_stream)
1400 output_stream.write('\n')
1401
1402 return results
1403
1404
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001405class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001406 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001407 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001408 """
1409 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001410 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001411 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001412 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001413 gerrit_obj: provides basic Gerrit codereview functionality.
1414 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001415 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001416 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001417 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001418 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001419 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001420 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001421 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001422
1423 def ExecPresubmitScript(self, script_text, presubmit_path):
1424 """Executes a single presubmit script.
1425
1426 Args:
1427 script_text: The text of the presubmit script.
1428 presubmit_path: The path to the presubmit file (this will be reported via
1429 input_api.PresubmitLocalPath()).
1430
1431 Return:
1432 A list of result objects, empty if no problems.
1433 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001434
chase@chromium.org8e416c82009-10-06 04:30:44 +00001435 # Change to the presubmit file's directory to support local imports.
1436 main_path = os.getcwd()
1437 os.chdir(os.path.dirname(presubmit_path))
1438
1439 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001440 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001441 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001442 gerrit_obj=self.gerrit, dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001443 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001444 try:
1445 exec script_text in context
1446 except Exception, e:
1447 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001448
1449 # These function names must change if we make substantial changes to
1450 # the presubmit API that are not backwards compatible.
1451 if self.committing:
1452 function_name = 'CheckChangeOnCommit'
1453 else:
1454 function_name = 'CheckChangeOnUpload'
1455 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001456 context['__args'] = (input_api, OutputApi(self.committing))
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001457 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001458 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001459 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001460 if not (isinstance(result, types.TupleType) or
1461 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001462 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001463 'Presubmit functions must return a tuple or list')
1464 for item in result:
1465 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001466 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001467 'All presubmit results must be of types derived from '
1468 'output_api.PresubmitResult')
1469 else:
1470 result = () # no error since the script doesn't care about current event.
1471
chase@chromium.org8e416c82009-10-06 04:30:44 +00001472 # Return the process to the original working directory.
1473 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001474 return result
1475
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001476
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001477def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001478 committing,
1479 verbose,
1480 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001481 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001482 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001483 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001484 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001485 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001486 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001487 """Runs all presubmit checks that apply to the files in the change.
1488
1489 This finds all PRESUBMIT.py files in directories enclosing the files in the
1490 change (up to the repository root) and calls the relevant entrypoint function
1491 depending on whether the change is being committed or uploaded.
1492
1493 Prints errors, warnings and notifications. Prompts the user for warnings
1494 when needed.
1495
1496 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001497 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001498 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1499 verbose: Prints debug info.
1500 output_stream: A stream to write output from presubmit tests to.
1501 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001502 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001503 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001504 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001505 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001506 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001507
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001508 Warning:
1509 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1510 SHOULD be sys.stdin.
1511
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001512 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001513 A PresubmitOutput object. Use output.should_continue() to figure out
1514 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001515 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001516 old_environ = os.environ
1517 try:
1518 # Make sure python subprocesses won't generate .pyc files.
1519 os.environ = os.environ.copy()
1520 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001521
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001522 output = PresubmitOutput(input_stream, output_stream)
1523 if committing:
1524 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001525 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001526 output.write("Running presubmit upload checks ...\n")
1527 start_time = time.time()
1528 presubmit_files = ListRelevantPresubmitFiles(
1529 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1530 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001531 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001532 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001533 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001534 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001535 if default_presubmit:
1536 if verbose:
1537 output.write("Running default presubmit script.\n")
1538 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1539 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1540 for filename in presubmit_files:
1541 filename = os.path.abspath(filename)
1542 if verbose:
1543 output.write("Running %s\n" % filename)
1544 # Accept CRLF presubmit script.
1545 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1546 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001547
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001548 errors = []
1549 notifications = []
1550 warnings = []
1551 for result in results:
1552 if result.fatal:
1553 errors.append(result)
1554 elif result.should_prompt:
1555 warnings.append(result)
1556 else:
1557 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001558
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001559 output.write('\n')
1560 for name, items in (('Messages', notifications),
1561 ('Warnings', warnings),
1562 ('ERRORS', errors)):
1563 if items:
1564 output.write('** Presubmit %s **\n' % name)
1565 for item in items:
1566 item.handle(output)
1567 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001568
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001569 total_time = time.time() - start_time
1570 if total_time > 1.0:
1571 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001572
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001573 if not errors:
1574 if not warnings:
1575 output.write('Presubmit checks passed.\n')
1576 elif may_prompt:
1577 output.prompt_yes_no('There were presubmit warnings. '
1578 'Are you sure you wish to continue? (y/N): ')
1579 else:
1580 output.fail()
1581
1582 global _ASKED_FOR_FEEDBACK
1583 # Ask for feedback one time out of 5.
1584 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001585 output.write(
1586 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1587 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1588 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001589 _ASKED_FOR_FEEDBACK = True
1590 return output
1591 finally:
1592 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001593
1594
1595def ScanSubDirs(mask, recursive):
1596 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001597 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001598 else:
1599 results = []
1600 for root, dirs, files in os.walk('.'):
1601 if '.svn' in dirs:
1602 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001603 if '.git' in dirs:
1604 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001605 for name in files:
1606 if fnmatch.fnmatch(name, mask):
1607 results.append(os.path.join(root, name))
1608 return results
1609
1610
1611def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001612 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001613 files = []
1614 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001615 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001616 return files
1617
1618
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001619def load_files(options, args):
1620 """Tries to determine the SCM."""
1621 change_scm = scm.determine_scm(options.root)
1622 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001623 if args:
1624 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001625 if change_scm == 'svn':
1626 change_class = SvnChange
1627 if not files:
1628 files = scm.SVN.CaptureStatus([], options.root)
1629 elif change_scm == 'git':
1630 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001631 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001632 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001633 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001634 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001635 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1636 if not files:
1637 return None, None
1638 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001639 return change_class, files
1640
1641
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001642class NonexistantCannedCheckFilter(Exception):
1643 pass
1644
1645
1646@contextlib.contextmanager
1647def canned_check_filter(method_names):
1648 filtered = {}
1649 try:
1650 for method_name in method_names:
1651 if not hasattr(presubmit_canned_checks, method_name):
1652 raise NonexistantCannedCheckFilter(method_name)
1653 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1654 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1655 yield
1656 finally:
1657 for name, method in filtered.iteritems():
1658 setattr(presubmit_canned_checks, name, method)
1659
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001660
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001661def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001662 """Runs an external program, potentially from a child process created by the
1663 multiprocessing module.
1664
1665 multiprocessing needs a top level function with a single argument.
1666 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001667 cmd_data.kwargs['stdout'] = subprocess.PIPE
1668 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1669 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001670 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001671 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001672 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001673 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001674 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001675 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001676 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1677 if code != 0:
1678 return cmd_data.message(
1679 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1680 if cmd_data.info:
1681 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001682
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001683
sbc@chromium.org013731e2015-02-26 18:28:43 +00001684def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001685 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001686 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001687 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001688 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001689 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1690 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001691 parser.add_option("-r", "--recursive", action="store_true",
1692 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001693 parser.add_option("-v", "--verbose", action="count", default=0,
1694 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001695 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001696 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001697 parser.add_option("--description", default='')
1698 parser.add_option("--issue", type='int', default=0)
1699 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001700 parser.add_option("--root", default=os.getcwd(),
1701 help="Search for PRESUBMIT.py up to this directory. "
1702 "If inherit-review-settings-ok is present in this "
1703 "directory, parent directories up to the root file "
1704 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001705 parser.add_option("--upstream",
1706 help="Git only: the base ref or upstream branch against "
1707 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001708 parser.add_option("--default_presubmit")
1709 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001710 parser.add_option("--skip_canned", action='append', default=[],
1711 help="A list of checks to skip which appear in "
1712 "presubmit_canned_checks. Can be provided multiple times "
1713 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001714 parser.add_option("--dry_run", action='store_true',
1715 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001716 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001717 parser.add_option("--gerrit_fetch", action='store_true',
1718 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001719 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1720 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001721 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1722 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001723 # These are for OAuth2 authentication for bots. See also apply_issue.py
1724 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1725 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1726
phajdan.jr@chromium.org2ff30182016-03-23 09:52:51 +00001727 # TODO(phajdan.jr): Update callers and remove obsolete --trybot-json .
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001728 parser.add_option("--trybot-json",
1729 help="Output trybot information to the file specified.")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001730 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001731 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001732 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001733
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001734 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001735 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001736 elif options.verbose:
1737 logging.basicConfig(level=logging.INFO)
1738 else:
1739 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001740
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001741 if (any((options.rietveld_url, options.rietveld_email_file,
1742 options.rietveld_fetch, options.rietveld_private_key_file))
1743 and any((options.gerrit_url, options.gerrit_fetch))):
1744 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1745 'allowed')
1746
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001747 if options.rietveld_email and options.rietveld_email_file:
1748 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1749 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001750 if options.rietveld_email_file:
1751 with open(options.rietveld_email_file, "rb") as f:
1752 options.rietveld_email = f.read().strip()
1753
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001754 change_class, files = load_files(options, args)
1755 if not change_class:
1756 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001757 logging.info('Found %d file(s).' % len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001758
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001759 rietveld_obj, gerrit_obj = None, None
1760
maruel@chromium.org239f4112011-06-03 20:08:23 +00001761 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001762 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001763 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001764 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1765 options.rietveld_url,
1766 options.rietveld_email,
1767 options.rietveld_private_key_file)
1768 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001769 rietveld_obj = rietveld.CachingRietveld(
1770 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001771 auth_config,
1772 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001773 if options.rietveld_fetch:
1774 assert options.issue
1775 props = rietveld_obj.get_issue_properties(options.issue, False)
1776 options.author = props['owner_email']
1777 options.description = props['description']
1778 logging.info('Got author: "%s"', options.author)
1779 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001780
1781 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001782 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001783 rietveld_obj = None
1784 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1785 options.author = gerrit_obj.GetChangeOwner(options.issue)
1786 options.description = gerrit_obj.GetChangeDescription(options.issue,
1787 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001788 logging.info('Got author: "%s"', options.author)
1789 logging.info('Got description: """\n%s\n"""', options.description)
1790
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001791 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001792 with canned_check_filter(options.skip_canned):
1793 results = DoPresubmitChecks(
1794 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001795 options.description,
1796 options.root,
1797 files,
1798 options.issue,
1799 options.patchset,
1800 options.author,
1801 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001802 options.commit,
1803 options.verbose,
1804 sys.stdout,
1805 sys.stdin,
1806 options.default_presubmit,
1807 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001808 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001809 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001810 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001811 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001812 except NonexistantCannedCheckFilter, e:
1813 print >> sys.stderr, (
1814 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1815 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001816 except PresubmitFailure, e:
1817 print >> sys.stderr, e
1818 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1819 print >> sys.stderr, 'If all fails, contact maruel@'
1820 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001821
1822
1823if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001824 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001825 try:
1826 sys.exit(main())
1827 except KeyboardInterrupt:
1828 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001829 sys.exit(2)