blob: 4f72d5c8604974d4e9b43e6ded0ad3049b6454c3 [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):
247 # Gerrit has 'approved' sub-section, but it only lists 1 approver.
248 # So, if we look only for approvers, we have to look at all anyway.
249 # Also, assume LGTM means Code-Review label == 2. Other configurations
250 # aren't supported.
251 return [r['email']
252 for r in self.GetChangeInfo(issue)['labels']['Code-Review']['all']
253 if not approving_only or '2' == str(r.get('value', 0))]
254
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000255
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000257 """An instance of OutputApi gets passed to presubmit scripts so that they
258 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000259 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000260 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000261 PresubmitError = _PresubmitError
262 PresubmitPromptWarning = _PresubmitPromptWarning
263 PresubmitNotifyResult = _PresubmitNotifyResult
264 MailTextResult = _MailTextResult
265
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000266 def __init__(self, is_committing):
267 self.is_committing = is_committing
268
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000269 def PresubmitPromptOrNotify(self, *args, **kwargs):
270 """Warn the user when uploading, but only notify if committing."""
271 if self.is_committing:
272 return self.PresubmitNotifyResult(*args, **kwargs)
273 return self.PresubmitPromptWarning(*args, **kwargs)
274
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000275
276class InputApi(object):
277 """An instance of this object is passed to presubmit scripts so they can
278 know stuff about the change they're looking at.
279 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000280 # Method could be a function
281 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000282
maruel@chromium.org3410d912009-06-09 20:56:16 +0000283 # File extensions that are considered source files from a style guide
284 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000285 #
286 # Files without an extension aren't included in the list. If you want to
287 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
288 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000289 DEFAULT_WHITE_LIST = (
290 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000291 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
292 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000293 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000294 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000295 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000296 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000297 )
298
299 # Path regexp that should be excluded from being considered containing source
300 # files. Don't modify this list from a presubmit script!
301 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000302 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000303 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000304 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
305 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000306 # Output directories (just in case)
307 r".*\bDebug[\\\/].*",
308 r".*\bRelease[\\\/].*",
309 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000310 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000311 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000312 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000313 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000314 r"(|.*[\\\/])\.git[\\\/].*",
315 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000316 # There is no point in processing a patch file.
317 r".+\.diff$",
318 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000319 )
320
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000321 def __init__(self, change, presubmit_path, is_committing,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000322 rietveld_obj, verbose, gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000323 """Builds an InputApi object.
324
325 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000326 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000327 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000328 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000329 rietveld_obj: rietveld.Rietveld client object
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000330 gerrit_obj: provides basic Gerrit codereview functionality.
331 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000332 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000333 # Version number of the presubmit_support script.
334 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000335 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000336 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000337 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000338 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000339 self.dry_run = dry_run
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000340 # TBD
341 self.host_url = 'http://codereview.chromium.org'
342 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000343 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000344
345 # We expose various modules and functions as attributes of the input_api
346 # so that presubmit scripts don't have to import them.
347 self.basename = os.path.basename
348 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000349 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000350 self.cStringIO = cStringIO
dcheng091b7db2016-06-16 01:27:51 -0700351 self.fnmatch = fnmatch
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000352 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000353 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000354 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000355 self.os_listdir = os.listdir
356 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000357 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000358 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000359 self.pickle = pickle
360 self.marshal = marshal
361 self.re = re
362 self.subprocess = subprocess
363 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000364 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000365 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000366 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000367 self.urllib2 = urllib2
368
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000369 # To easily fork python.
370 self.python_executable = sys.executable
371 self.environ = os.environ
372
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000373 # InputApi.platform is the platform you're currently running on.
374 self.platform = sys.platform
375
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000376 self.cpu_count = multiprocessing.cpu_count()
377
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000378 # this is done here because in RunTests, the current working directory has
379 # changed, which causes Pool() to explode fantastically when run on windows
380 # (because it tries to load the __main__ module, which imports lots of
381 # things relative to the current working directory).
382 self._run_tests_pool = multiprocessing.Pool(self.cpu_count)
383
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000384 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000385 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000386
387 # We carry the canned checks so presubmit scripts can easily use them.
388 self.canned_checks = presubmit_canned_checks
389
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000390 # TODO(dpranke): figure out a list of all approved owners for a repo
391 # in order to be able to handle wildcard OWNERS files?
392 self.owners_db = owners.Database(change.RepositoryRoot(),
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000393 fopen=file, os_path=self.os_path, glob=self.glob)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000394 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000395 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000396
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000397 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000398 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000399 # Access to a protected member _XX of a client class
400 # pylint: disable=W0212
401 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000402 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000403 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
404 for (a, b, header) in cpplint._re_pattern_templates
405 ]
406
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000407 def PresubmitLocalPath(self):
408 """Returns the local path of the presubmit script currently being run.
409
410 This is useful if you don't want to hard-code absolute paths in the
411 presubmit script. For example, It can be used to find another file
412 relative to the PRESUBMIT.py script, so the whole tree can be branched and
413 the presubmit script still works, without editing its content.
414 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000415 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000416
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000417 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000418 """Translate a depot path to a local path (relative to client root).
419
420 Args:
421 Depot path as a string.
422
423 Returns:
424 The local path of the depot path under the user's current client, or None
425 if the file is not mapped.
426
427 Remember to check for the None case and show an appropriate error!
428 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000429 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
430 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000431
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000432 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000433 """Translate a local path to a depot path.
434
435 Args:
436 Local path (relative to current directory, or absolute) as a string.
437
438 Returns:
439 The depot path (SVN URL) of the file if mapped, otherwise None.
440 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000441 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
442 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000443
sail@chromium.org5538e022011-05-12 17:53:16 +0000444 def AffectedFiles(self, include_dirs=False, include_deletes=True,
445 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000446 """Same as input_api.change.AffectedFiles() except only lists files
447 (and optionally directories) in the same directory as the current presubmit
448 script, or subdirectories thereof.
449 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000450 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000451 if len(dir_with_slash) == 1:
452 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000453
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000454 return filter(
455 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000456 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000457
458 def LocalPaths(self, include_dirs=False):
459 """Returns local paths of input_api.AffectedFiles()."""
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000460 paths = [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
461 logging.debug("LocalPaths: %s", paths)
462 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000463
464 def AbsoluteLocalPaths(self, include_dirs=False):
465 """Returns absolute local paths of input_api.AffectedFiles()."""
466 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
467
468 def ServerPaths(self, include_dirs=False):
469 """Returns server paths of input_api.AffectedFiles()."""
470 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
471
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000472 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000473 """Same as input_api.change.AffectedTextFiles() except only lists files
474 in the same directory as the current presubmit script, or subdirectories
475 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000476 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000477 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000478 warn("AffectedTextFiles(include_deletes=%s)"
479 " is deprecated and ignored" % str(include_deletes),
480 category=DeprecationWarning,
481 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000482 return filter(lambda x: x.IsTextFile(),
483 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000484
maruel@chromium.org3410d912009-06-09 20:56:16 +0000485 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
486 """Filters out files that aren't considered "source file".
487
488 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
489 and InputApi.DEFAULT_BLACK_LIST is used respectively.
490
491 The lists will be compiled as regular expression and
492 AffectedFile.LocalPath() needs to pass both list.
493
494 Note: Copy-paste this function to suit your needs or use a lambda function.
495 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000496 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000497 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000498 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000499 if self.re.match(item, local_path):
500 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000501 return True
502 return False
503 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
504 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
505
506 def AffectedSourceFiles(self, source_file):
507 """Filter the list of AffectedTextFiles by the function source_file.
508
509 If source_file is None, InputApi.FilterSourceFile() is used.
510 """
511 if not source_file:
512 source_file = self.FilterSourceFile
513 return filter(source_file, self.AffectedTextFiles())
514
515 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000516 """An iterator over all text lines in "new" version of changed files.
517
518 Only lists lines from new or modified text files in the change that are
519 contained by the directory of the currently executing presubmit script.
520
521 This is useful for doing line-by-line regex checks, like checking for
522 trailing whitespace.
523
524 Yields:
525 a 3 tuple:
526 the AffectedFile instance of the current file;
527 integer line number (1-based); and
528 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000529
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000530 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000531 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000532 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000533 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000534
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000535 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000536 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000537
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000538 Deny reading anything outside the repository.
539 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000540 if isinstance(file_item, AffectedFile):
541 file_item = file_item.AbsoluteLocalPath()
542 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000543 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000544 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000545
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000546 @property
547 def tbr(self):
548 """Returns if a change is TBR'ed."""
549 return 'TBR' in self.change.tags
550
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000551 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000552 tests = []
553 msgs = []
554 for t in tests_mix:
555 if isinstance(t, OutputApi.PresubmitResult):
556 msgs.append(t)
557 else:
558 assert issubclass(t.message, _PresubmitResult)
559 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000560 if self.verbose:
561 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000562 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000563 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000564 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000565 else:
566 msgs.extend(map(CallCommand, tests))
567 return [m for m in msgs if m]
568
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000569
nick@chromium.orgff526192013-06-10 19:30:26 +0000570class _DiffCache(object):
571 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000572 def __init__(self, upstream=None):
573 """Stores the upstream revision against which all diffs will be computed."""
574 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000575
576 def GetDiff(self, path, local_root):
577 """Get the diff for a particular path."""
578 raise NotImplementedError()
579
580
581class _SvnDiffCache(_DiffCache):
582 """DiffCache implementation for subversion."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000583 def __init__(self, *args, **kwargs):
584 super(_SvnDiffCache, self).__init__(*args, **kwargs)
nick@chromium.orgff526192013-06-10 19:30:26 +0000585 self._diffs_by_file = {}
586
587 def GetDiff(self, path, local_root):
588 if path not in self._diffs_by_file:
589 self._diffs_by_file[path] = scm.SVN.GenerateDiff([path], local_root,
590 False, None)
591 return self._diffs_by_file[path]
592
593
594class _GitDiffCache(_DiffCache):
595 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000596 def __init__(self, upstream):
597 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000598 self._diffs_by_file = None
599
600 def GetDiff(self, path, local_root):
601 if not self._diffs_by_file:
602 # Compute a single diff for all files and parse the output; should
603 # with git this is much faster than computing one diff for each file.
604 diffs = {}
605
606 # Don't specify any filenames below, because there are command line length
607 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000608 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
609 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000610
611 # This regex matches the path twice, separated by a space. Note that
612 # filename itself may contain spaces.
613 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
614 current_diff = []
615 keep_line_endings = True
616 for x in unified_diff.splitlines(keep_line_endings):
617 match = file_marker.match(x)
618 if match:
619 # Marks the start of a new per-file section.
620 diffs[match.group('filename')] = current_diff = [x]
621 elif x.startswith('diff --git'):
622 raise PresubmitFailure('Unexpected diff line: %s' % x)
623 else:
624 current_diff.append(x)
625
626 self._diffs_by_file = dict(
627 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
628
629 if path not in self._diffs_by_file:
630 raise PresubmitFailure(
631 'Unified diff did not contain entry for file %s' % path)
632
633 return self._diffs_by_file[path]
634
635
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000636class AffectedFile(object):
637 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000638
639 DIFF_CACHE = _DiffCache
640
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000641 # Method could be a function
642 # pylint: disable=R0201
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000643 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000644 self._path = path
645 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000646 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000647 self._is_directory = None
648 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000649 self._cached_changed_contents = None
650 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000651 self._diff_cache = diff_cache
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000652 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000653
654 def ServerPath(self):
655 """Returns a path string that identifies the file in the SCM system.
656
657 Returns the empty string if the file does not exist in SCM.
658 """
nick@chromium.orgff526192013-06-10 19:30:26 +0000659 return ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000660
661 def LocalPath(self):
662 """Returns the path of this file on the local disk relative to client root.
663 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000664 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000665
666 def AbsoluteLocalPath(self):
667 """Returns the absolute path of this file on the local disk.
668 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000669 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000670
671 def IsDirectory(self):
672 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000673 if self._is_directory is None:
674 path = self.AbsoluteLocalPath()
675 self._is_directory = (os.path.exists(path) and
676 os.path.isdir(path))
677 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000678
679 def Action(self):
680 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000681 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
682 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000683 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000685 def Property(self, property_name):
686 """Returns the specified SCM property of this file, or None if no such
687 property.
688 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000689 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000690
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000691 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000692 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000693
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000694 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000695 raise NotImplementedError() # Implement when needed
696
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000697 def NewContents(self):
698 """Returns an iterator over the lines in the new version of file.
699
700 The new version is the file in the user's workspace, i.e. the "right hand
701 side".
702
703 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000704 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000705 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000706 if self._cached_new_contents is None:
707 self._cached_new_contents = []
708 if not self.IsDirectory():
709 try:
710 self._cached_new_contents = gclient_utils.FileRead(
711 self.AbsoluteLocalPath(), 'rU').splitlines()
712 except IOError:
713 pass # File not found? That's fine; maybe it was deleted.
714 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000715
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000716 def ChangedContents(self):
717 """Returns a list of tuples (line number, line text) of all new lines.
718
719 This relies on the scm diff output describing each changed code section
720 with a line of the form
721
722 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
723 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000724 if self._cached_changed_contents is not None:
725 return self._cached_changed_contents[:]
726 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000727 line_num = 0
728
729 if self.IsDirectory():
730 return []
731
732 for line in self.GenerateScmDiff().splitlines():
733 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
734 if m:
735 line_num = int(m.groups(1)[0])
736 continue
737 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000738 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000739 if not line.startswith('-'):
740 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000741 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000742
maruel@chromium.org5de13972009-06-10 18:16:06 +0000743 def __str__(self):
744 return self.LocalPath()
745
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000746 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000747 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000748
maruel@chromium.org58407af2011-04-12 23:15:57 +0000749
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000750class SvnAffectedFile(AffectedFile):
751 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000752 # Method 'NNN' is abstract in class 'NNN' but is not overridden
753 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000754
nick@chromium.orgff526192013-06-10 19:30:26 +0000755 DIFF_CACHE = _SvnDiffCache
756
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000757 def __init__(self, *args, **kwargs):
758 AffectedFile.__init__(self, *args, **kwargs)
759 self._server_path = None
760 self._is_text_file = None
761
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000762 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000763 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000764 self._server_path = scm.SVN.CaptureLocalInfo(
765 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000766 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000767
768 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000769 if self._is_directory is None:
770 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000771 if os.path.exists(path):
772 # Retrieve directly from the file system; it is much faster than
773 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000774 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000775 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000776 self._is_directory = scm.SVN.CaptureLocalInfo(
777 [self.LocalPath()], self._local_root
778 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000779 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000780
781 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000782 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000783 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000784 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000785 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000786
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000787 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000788 if self._is_text_file is None:
789 if self.Action() == 'D':
790 # A deleted file is not a text file.
791 self._is_text_file = False
792 elif self.IsDirectory():
793 self._is_text_file = False
794 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000795 mime_type = scm.SVN.GetFileProperty(
796 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000797 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
798 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000799
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000800
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000801class GitAffectedFile(AffectedFile):
802 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000803 # Method 'NNN' is abstract in class 'NNN' but is not overridden
804 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000805
nick@chromium.orgff526192013-06-10 19:30:26 +0000806 DIFF_CACHE = _GitDiffCache
807
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000808 def __init__(self, *args, **kwargs):
809 AffectedFile.__init__(self, *args, **kwargs)
810 self._server_path = None
811 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000812
813 def ServerPath(self):
814 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000815 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000816 return self._server_path
817
818 def IsDirectory(self):
819 if self._is_directory is None:
820 path = self.AbsoluteLocalPath()
821 if os.path.exists(path):
822 # Retrieve directly from the file system; it is much faster than
823 # querying subversion, especially on Windows.
824 self._is_directory = os.path.isdir(path)
825 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000826 self._is_directory = False
827 return self._is_directory
828
829 def Property(self, property_name):
830 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000831 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000832 return self._properties[property_name]
833
834 def IsTextFile(self):
835 if self._is_text_file is None:
836 if self.Action() == 'D':
837 # A deleted file is not a text file.
838 self._is_text_file = False
839 elif self.IsDirectory():
840 self._is_text_file = False
841 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000842 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
843 return self._is_text_file
844
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000845
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000846class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000847 """Describe a change.
848
849 Used directly by the presubmit scripts to query the current change being
850 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000851
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000852 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000853 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000854 self.KEY: equivalent to tags['KEY']
855 """
856
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000857 _AFFECTED_FILES = AffectedFile
858
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000859 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000860 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000861 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000862 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000863
maruel@chromium.org58407af2011-04-12 23:15:57 +0000864 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000865 self, name, description, local_root, files, issue, patchset, author,
866 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000867 if files is None:
868 files = []
869 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000870 # Convert root into an absolute path.
871 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000872 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000873 self.issue = issue
874 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000875 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000876
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000877 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000878 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000879 self._description_without_tags = ''
880 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000881
maruel@chromium.orge085d812011-10-10 19:49:15 +0000882 assert all(
883 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
884
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000885 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000886 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000887 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
888 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000889 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000890
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000891 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000892 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000893 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000894
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000895 def DescriptionText(self):
896 """Returns the user-entered changelist description, minus tags.
897
898 Any line in the user-provided description starting with e.g. "FOO="
899 (whitespace permitted before and around) is considered a tag line. Such
900 lines are stripped out of the description this function returns.
901 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000902 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000903
904 def FullDescriptionText(self):
905 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000906 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000907
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000908 def SetDescriptionText(self, description):
909 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000910
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000911 Also updates the list of tags."""
912 self._full_description = description
913
914 # From the description text, build up a dictionary of key/value pairs
915 # plus the description minus all key/value or "tag" lines.
916 description_without_tags = []
917 self.tags = {}
918 for line in self._full_description.splitlines():
919 m = self.TAG_LINE_RE.match(line)
920 if m:
921 self.tags[m.group('key')] = m.group('value')
922 else:
923 description_without_tags.append(line)
924
925 # Change back to text and remove whitespace at end.
926 self._description_without_tags = (
927 '\n'.join(description_without_tags).rstrip())
928
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000929 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000930 """Returns the repository (checkout) root directory for this change,
931 as an absolute path.
932 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000933 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000934
935 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000936 """Return tags directly as attributes on the object."""
937 if not re.match(r"^[A-Z_]*$", attr):
938 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000939 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000940
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000941 def AllFiles(self, root=None):
942 """List all files under source control in the repo."""
943 raise NotImplementedError()
944
sail@chromium.org5538e022011-05-12 17:53:16 +0000945 def AffectedFiles(self, include_dirs=False, include_deletes=True,
946 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000947 """Returns a list of AffectedFile instances for all files in the change.
948
949 Args:
950 include_deletes: If false, deleted files will be filtered out.
951 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000952 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000953
954 Returns:
955 [AffectedFile(path, action), AffectedFile(path, action)]
956 """
957 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000958 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000959 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000960 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000961
sail@chromium.org5538e022011-05-12 17:53:16 +0000962 affected = filter(file_filter, affected)
963
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000964 if include_deletes:
965 return affected
966 else:
967 return filter(lambda x: x.Action() != 'D', affected)
968
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000969 def AffectedTextFiles(self, include_deletes=None):
970 """Return a list of the existing text files in a change."""
971 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000972 warn("AffectedTextFiles(include_deletes=%s)"
973 " is deprecated and ignored" % str(include_deletes),
974 category=DeprecationWarning,
975 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000976 return filter(lambda x: x.IsTextFile(),
977 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000978
979 def LocalPaths(self, include_dirs=False):
980 """Convenience function."""
981 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
982
983 def AbsoluteLocalPaths(self, include_dirs=False):
984 """Convenience function."""
985 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
986
987 def ServerPaths(self, include_dirs=False):
988 """Convenience function."""
989 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
990
991 def RightHandSideLines(self):
992 """An iterator over all text lines in "new" version of changed files.
993
994 Lists lines from new or modified text files in the change.
995
996 This is useful for doing line-by-line regex checks, like checking for
997 trailing whitespace.
998
999 Yields:
1000 a 3 tuple:
1001 the AffectedFile instance of the current file;
1002 integer line number (1-based); and
1003 the contents of the line as a string.
1004 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001005 return _RightHandSideLinesImpl(
1006 x for x in self.AffectedFiles(include_deletes=False)
1007 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008
1009
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001010class SvnChange(Change):
1011 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001012 scm = 'svn'
1013 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +00001014
1015 def _GetChangeLists(self):
1016 """Get all change lists."""
1017 if self._changelists == None:
1018 previous_cwd = os.getcwd()
1019 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001020 # Need to import here to avoid circular dependency.
1021 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +00001022 self._changelists = gcl.GetModifiedFiles()
1023 os.chdir(previous_cwd)
1024 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001025
1026 def GetAllModifiedFiles(self):
1027 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +00001028 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001029 all_modified_files = []
1030 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +00001031 all_modified_files.extend(
1032 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001033 return all_modified_files
1034
1035 def GetModifiedFiles(self):
1036 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +00001037 changelists = self._GetChangeLists()
1038 return [os.path.join(self.RepositoryRoot(), f[1])
1039 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001040
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001041 def AllFiles(self, root=None):
1042 """List all files under source control in the repo."""
1043 root = root or self.RepositoryRoot()
1044 return subprocess.check_output(
1045 ['svn', 'ls', '-R', '.'], cwd=root).splitlines()
1046
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001047
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001048class GitChange(Change):
1049 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001050 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001051
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001052 def AllFiles(self, root=None):
1053 """List all files under source control in the repo."""
1054 root = root or self.RepositoryRoot()
1055 return subprocess.check_output(
1056 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
1057
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001058
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001059def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001060 """Finds all presubmit files that apply to a given set of source files.
1061
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001062 If inherit-review-settings-ok is present right under root, looks for
1063 PRESUBMIT.py in directories enclosing root.
1064
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001065 Args:
1066 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001067 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001068
1069 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001070 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001071 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001072 files = [normpath(os.path.join(root, f)) for f in files]
1073
1074 # List all the individual directories containing files.
1075 directories = set([os.path.dirname(f) for f in files])
1076
1077 # Ignore root if inherit-review-settings-ok is present.
1078 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1079 root = None
1080
1081 # Collect all unique directories that may contain PRESUBMIT.py.
1082 candidates = set()
1083 for directory in directories:
1084 while True:
1085 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001086 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001087 candidates.add(directory)
1088 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001089 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001090 parent_dir = os.path.dirname(directory)
1091 if parent_dir == directory:
1092 # We hit the system root directory.
1093 break
1094 directory = parent_dir
1095
1096 # Look for PRESUBMIT.py in all candidate directories.
1097 results = []
1098 for directory in sorted(list(candidates)):
1099 p = os.path.join(directory, 'PRESUBMIT.py')
1100 if os.path.isfile(p):
1101 results.append(p)
1102
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001103 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001104 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001105
1106
thestig@chromium.orgde243452009-10-06 21:02:56 +00001107class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001108 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001109 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +00001110 """Executes GetPreferredTrySlaves() from a single presubmit script.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001111
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001112 This will soon be deprecated and replaced by GetPreferredTryMasters().
thestig@chromium.orgde243452009-10-06 21:02:56 +00001113
1114 Args:
1115 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +00001116 presubmit_path: Project script to run.
1117 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001118
1119 Return:
1120 A list of try slaves.
1121 """
1122 context = {}
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001123 main_path = os.getcwd()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001124 try:
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001125 os.chdir(os.path.dirname(presubmit_path))
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001126 exec script_text in context
1127 except Exception, e:
1128 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001129 finally:
1130 os.chdir(main_path)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001131
1132 function_name = 'GetPreferredTrySlaves'
1133 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001134 get_preferred_try_slaves = context[function_name]
1135 function_info = inspect.getargspec(get_preferred_try_slaves)
1136 if len(function_info[0]) == 1:
1137 result = get_preferred_try_slaves(project)
1138 elif len(function_info[0]) == 2:
1139 result = get_preferred_try_slaves(project, change)
1140 else:
1141 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +00001142 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001143 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +00001144 'Presubmit functions must return a list, got a %s instead: %s' %
1145 (type(result), str(result)))
1146 for item in result:
stip@chromium.org68e04192013-11-04 22:14:38 +00001147 if isinstance(item, basestring):
1148 # Old-style ['bot'] format.
1149 botname = item
1150 elif isinstance(item, tuple):
1151 # New-style [('bot', set(['tests']))] format.
1152 botname = item[0]
1153 else:
1154 raise PresubmitFailure('PRESUBMIT.py returned invalid tryslave/test'
1155 ' format.')
1156
1157 if botname != botname.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001158 raise PresubmitFailure(
1159 'Try slave names cannot start/end with whitespace')
stip@chromium.org68e04192013-11-04 22:14:38 +00001160 if ',' in botname:
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +00001161 raise PresubmitFailure(
stip@chromium.org68e04192013-11-04 22:14:38 +00001162 'Do not use \',\' separated builder or test names: %s' % botname)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001163 else:
1164 result = []
stip@chromium.org5ca27622013-12-18 17:44:58 +00001165
1166 def valid_oldstyle(result):
1167 return all(isinstance(i, basestring) for i in result)
1168
1169 def valid_newstyle(result):
1170 return (all(isinstance(i, tuple) for i in result) and
1171 all(len(i) == 2 for i in result) and
1172 all(isinstance(i[0], basestring) for i in result) and
1173 all(isinstance(i[1], set) for i in result)
1174 )
1175
1176 # Ensure it's either all old-style or all new-style.
1177 if not valid_oldstyle(result) and not valid_newstyle(result):
1178 raise PresubmitFailure(
1179 'PRESUBMIT.py returned invalid trybot specification!')
1180
thestig@chromium.orgde243452009-10-06 21:02:56 +00001181 return result
1182
1183
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001184class GetTryMastersExecuter(object):
1185 @staticmethod
1186 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1187 """Executes GetPreferredTryMasters() from a single presubmit script.
1188
1189 Args:
1190 script_text: The text of the presubmit script.
1191 presubmit_path: Project script to run.
1192 project: Project name to pass to presubmit script for bot selection.
1193
1194 Return:
1195 A map of try masters to map of builders to set of tests.
1196 """
1197 context = {}
1198 try:
1199 exec script_text in context
1200 except Exception, e:
1201 raise PresubmitFailure('"%s" had an exception.\n%s'
1202 % (presubmit_path, e))
1203
1204 function_name = 'GetPreferredTryMasters'
1205 if function_name not in context:
1206 return {}
1207 get_preferred_try_masters = context[function_name]
1208 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1209 raise PresubmitFailure(
1210 'Expected function "GetPreferredTryMasters" to take two arguments.')
1211 return get_preferred_try_masters(project, change)
1212
1213
rmistry@google.com5626a922015-02-26 14:03:30 +00001214class GetPostUploadExecuter(object):
1215 @staticmethod
1216 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1217 """Executes PostUploadHook() from a single presubmit script.
1218
1219 Args:
1220 script_text: The text of the presubmit script.
1221 presubmit_path: Project script to run.
1222 cl: The Changelist object.
1223 change: The Change object.
1224
1225 Return:
1226 A list of results objects.
1227 """
1228 context = {}
1229 try:
1230 exec script_text in context
1231 except Exception, e:
1232 raise PresubmitFailure('"%s" had an exception.\n%s'
1233 % (presubmit_path, e))
1234
1235 function_name = 'PostUploadHook'
1236 if function_name not in context:
1237 return {}
1238 post_upload_hook = context[function_name]
1239 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1240 raise PresubmitFailure(
1241 'Expected function "PostUploadHook" to take three arguments.')
1242 return post_upload_hook(cl, change, OutputApi(False))
1243
1244
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001245def DoGetTrySlaves(change,
1246 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001247 repository_root,
1248 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +00001249 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001250 verbose,
1251 output_stream):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001252 """Get the list of try servers from the presubmit scripts (deprecated).
thestig@chromium.orgde243452009-10-06 21:02:56 +00001253
1254 Args:
1255 changed_files: List of modified files.
1256 repository_root: The repository root.
1257 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +00001258 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001259 verbose: Prints debug info.
1260 output_stream: A stream to write debug output to.
1261
1262 Return:
1263 List of try slaves
1264 """
1265 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1266 if not presubmit_files and verbose:
mdempsky@chromium.orgd59e7612014-03-05 19:55:56 +00001267 output_stream.write("Warning, no PRESUBMIT.py found.\n")
thestig@chromium.orgde243452009-10-06 21:02:56 +00001268 results = []
1269 executer = GetTrySlavesExecuter()
stip@chromium.org5ca27622013-12-18 17:44:58 +00001270
thestig@chromium.orgde243452009-10-06 21:02:56 +00001271 if default_presubmit:
1272 if verbose:
1273 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001274 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001275 results.extend(executer.ExecPresubmitScript(
1276 default_presubmit, fake_path, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001277 for filename in presubmit_files:
1278 filename = os.path.abspath(filename)
1279 if verbose:
1280 output_stream.write("Running %s\n" % filename)
1281 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001282 presubmit_script = gclient_utils.FileRead(filename, 'rU')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001283 results.extend(executer.ExecPresubmitScript(
1284 presubmit_script, filename, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001285
stip@chromium.org5ca27622013-12-18 17:44:58 +00001286
1287 slave_dict = {}
1288 old_style = filter(lambda x: isinstance(x, basestring), results)
1289 new_style = filter(lambda x: isinstance(x, tuple), results)
1290
1291 for result in new_style:
1292 slave_dict.setdefault(result[0], set()).update(result[1])
1293 slaves = list(slave_dict.items())
1294
1295 slaves.extend(set(old_style))
stip@chromium.org68e04192013-11-04 22:14:38 +00001296
thestig@chromium.orgde243452009-10-06 21:02:56 +00001297 if slaves and verbose:
stip@chromium.org5ca27622013-12-18 17:44:58 +00001298 output_stream.write(', '.join((str(x) for x in slaves)))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001299 output_stream.write('\n')
1300 return slaves
1301
1302
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001303def _MergeMasters(masters1, masters2):
1304 """Merges two master maps. Merges also the tests of each builder."""
1305 result = {}
1306 for (master, builders) in itertools.chain(masters1.iteritems(),
1307 masters2.iteritems()):
1308 new_builders = result.setdefault(master, {})
1309 for (builder, tests) in builders.iteritems():
1310 new_builders.setdefault(builder, set([])).update(tests)
1311 return result
1312
1313
1314def DoGetTryMasters(change,
1315 changed_files,
1316 repository_root,
1317 default_presubmit,
1318 project,
1319 verbose,
1320 output_stream):
1321 """Get the list of try masters from the presubmit scripts.
1322
1323 Args:
1324 changed_files: List of modified files.
1325 repository_root: The repository root.
1326 default_presubmit: A default presubmit script to execute in any case.
1327 project: Optional name of a project used in selecting trybots.
1328 verbose: Prints debug info.
1329 output_stream: A stream to write debug output to.
1330
1331 Return:
1332 Map of try masters to map of builders to set of tests.
1333 """
1334 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1335 if not presubmit_files and verbose:
1336 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1337 results = {}
1338 executer = GetTryMastersExecuter()
1339
1340 if default_presubmit:
1341 if verbose:
1342 output_stream.write("Running default presubmit script.\n")
1343 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1344 results = _MergeMasters(results, executer.ExecPresubmitScript(
1345 default_presubmit, fake_path, project, change))
1346 for filename in presubmit_files:
1347 filename = os.path.abspath(filename)
1348 if verbose:
1349 output_stream.write("Running %s\n" % filename)
1350 # Accept CRLF presubmit script.
1351 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1352 results = _MergeMasters(results, executer.ExecPresubmitScript(
1353 presubmit_script, filename, project, change))
1354
1355 # Make sets to lists again for later JSON serialization.
1356 for builders in results.itervalues():
1357 for builder in builders:
1358 builders[builder] = list(builders[builder])
1359
1360 if results and verbose:
1361 output_stream.write('%s\n' % str(results))
1362 return results
1363
1364
rmistry@google.com5626a922015-02-26 14:03:30 +00001365def DoPostUploadExecuter(change,
1366 cl,
1367 repository_root,
1368 verbose,
1369 output_stream):
1370 """Execute the post upload hook.
1371
1372 Args:
1373 change: The Change object.
1374 cl: The Changelist object.
1375 repository_root: The repository root.
1376 verbose: Prints debug info.
1377 output_stream: A stream to write debug output to.
1378 """
1379 presubmit_files = ListRelevantPresubmitFiles(
1380 change.LocalPaths(), repository_root)
1381 if not presubmit_files and verbose:
1382 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1383 results = []
1384 executer = GetPostUploadExecuter()
1385 # The root presubmit file should be executed after the ones in subdirectories.
1386 # i.e. the specific post upload hooks should run before the general ones.
1387 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1388 presubmit_files.reverse()
1389
1390 for filename in presubmit_files:
1391 filename = os.path.abspath(filename)
1392 if verbose:
1393 output_stream.write("Running %s\n" % filename)
1394 # Accept CRLF presubmit script.
1395 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1396 results.extend(executer.ExecPresubmitScript(
1397 presubmit_script, filename, cl, change))
1398 output_stream.write('\n')
1399 if results:
1400 output_stream.write('** Post Upload Hook Messages **\n')
1401 for result in results:
1402 result.handle(output_stream)
1403 output_stream.write('\n')
1404
1405 return results
1406
1407
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001408class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001409 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001410 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001411 """
1412 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001413 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001414 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001415 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001416 gerrit_obj: provides basic Gerrit codereview functionality.
1417 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001418 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001419 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001420 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001421 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001422 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001423 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001424 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001425
1426 def ExecPresubmitScript(self, script_text, presubmit_path):
1427 """Executes a single presubmit script.
1428
1429 Args:
1430 script_text: The text of the presubmit script.
1431 presubmit_path: The path to the presubmit file (this will be reported via
1432 input_api.PresubmitLocalPath()).
1433
1434 Return:
1435 A list of result objects, empty if no problems.
1436 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001437
chase@chromium.org8e416c82009-10-06 04:30:44 +00001438 # Change to the presubmit file's directory to support local imports.
1439 main_path = os.getcwd()
1440 os.chdir(os.path.dirname(presubmit_path))
1441
1442 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001443 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001444 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001445 gerrit_obj=self.gerrit, dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001446 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001447 try:
1448 exec script_text in context
1449 except Exception, e:
1450 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001451
1452 # These function names must change if we make substantial changes to
1453 # the presubmit API that are not backwards compatible.
1454 if self.committing:
1455 function_name = 'CheckChangeOnCommit'
1456 else:
1457 function_name = 'CheckChangeOnUpload'
1458 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001459 context['__args'] = (input_api, OutputApi(self.committing))
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001460 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001461 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001462 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001463 if not (isinstance(result, types.TupleType) or
1464 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001465 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001466 'Presubmit functions must return a tuple or list')
1467 for item in result:
1468 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001469 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001470 'All presubmit results must be of types derived from '
1471 'output_api.PresubmitResult')
1472 else:
1473 result = () # no error since the script doesn't care about current event.
1474
chase@chromium.org8e416c82009-10-06 04:30:44 +00001475 # Return the process to the original working directory.
1476 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001477 return result
1478
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001479
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001480def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001481 committing,
1482 verbose,
1483 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001484 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001485 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001486 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001487 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001488 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001489 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001490 """Runs all presubmit checks that apply to the files in the change.
1491
1492 This finds all PRESUBMIT.py files in directories enclosing the files in the
1493 change (up to the repository root) and calls the relevant entrypoint function
1494 depending on whether the change is being committed or uploaded.
1495
1496 Prints errors, warnings and notifications. Prompts the user for warnings
1497 when needed.
1498
1499 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001500 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001501 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1502 verbose: Prints debug info.
1503 output_stream: A stream to write output from presubmit tests to.
1504 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001505 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001506 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001507 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001508 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001509 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001510
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001511 Warning:
1512 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1513 SHOULD be sys.stdin.
1514
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001515 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001516 A PresubmitOutput object. Use output.should_continue() to figure out
1517 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001518 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001519 old_environ = os.environ
1520 try:
1521 # Make sure python subprocesses won't generate .pyc files.
1522 os.environ = os.environ.copy()
1523 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001524
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001525 output = PresubmitOutput(input_stream, output_stream)
1526 if committing:
1527 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001528 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001529 output.write("Running presubmit upload checks ...\n")
1530 start_time = time.time()
1531 presubmit_files = ListRelevantPresubmitFiles(
1532 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1533 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001534 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001535 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001536 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001537 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001538 if default_presubmit:
1539 if verbose:
1540 output.write("Running default presubmit script.\n")
1541 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1542 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1543 for filename in presubmit_files:
1544 filename = os.path.abspath(filename)
1545 if verbose:
1546 output.write("Running %s\n" % filename)
1547 # Accept CRLF presubmit script.
1548 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1549 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001550
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001551 errors = []
1552 notifications = []
1553 warnings = []
1554 for result in results:
1555 if result.fatal:
1556 errors.append(result)
1557 elif result.should_prompt:
1558 warnings.append(result)
1559 else:
1560 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001561
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001562 output.write('\n')
1563 for name, items in (('Messages', notifications),
1564 ('Warnings', warnings),
1565 ('ERRORS', errors)):
1566 if items:
1567 output.write('** Presubmit %s **\n' % name)
1568 for item in items:
1569 item.handle(output)
1570 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001571
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001572 total_time = time.time() - start_time
1573 if total_time > 1.0:
1574 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001575
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001576 if not errors:
1577 if not warnings:
1578 output.write('Presubmit checks passed.\n')
1579 elif may_prompt:
1580 output.prompt_yes_no('There were presubmit warnings. '
1581 'Are you sure you wish to continue? (y/N): ')
1582 else:
1583 output.fail()
1584
1585 global _ASKED_FOR_FEEDBACK
1586 # Ask for feedback one time out of 5.
1587 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001588 output.write(
1589 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1590 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1591 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001592 _ASKED_FOR_FEEDBACK = True
1593 return output
1594 finally:
1595 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001596
1597
1598def ScanSubDirs(mask, recursive):
1599 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001600 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001601 else:
1602 results = []
1603 for root, dirs, files in os.walk('.'):
1604 if '.svn' in dirs:
1605 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001606 if '.git' in dirs:
1607 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001608 for name in files:
1609 if fnmatch.fnmatch(name, mask):
1610 results.append(os.path.join(root, name))
1611 return results
1612
1613
1614def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001615 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001616 files = []
1617 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001618 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001619 return files
1620
1621
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001622def load_files(options, args):
1623 """Tries to determine the SCM."""
1624 change_scm = scm.determine_scm(options.root)
1625 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001626 if args:
1627 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001628 if change_scm == 'svn':
1629 change_class = SvnChange
1630 if not files:
1631 files = scm.SVN.CaptureStatus([], options.root)
1632 elif change_scm == 'git':
1633 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001634 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001635 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001636 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001637 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001638 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1639 if not files:
1640 return None, None
1641 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001642 return change_class, files
1643
1644
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001645class NonexistantCannedCheckFilter(Exception):
1646 pass
1647
1648
1649@contextlib.contextmanager
1650def canned_check_filter(method_names):
1651 filtered = {}
1652 try:
1653 for method_name in method_names:
1654 if not hasattr(presubmit_canned_checks, method_name):
1655 raise NonexistantCannedCheckFilter(method_name)
1656 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1657 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1658 yield
1659 finally:
1660 for name, method in filtered.iteritems():
1661 setattr(presubmit_canned_checks, name, method)
1662
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001663
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001664def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001665 """Runs an external program, potentially from a child process created by the
1666 multiprocessing module.
1667
1668 multiprocessing needs a top level function with a single argument.
1669 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001670 cmd_data.kwargs['stdout'] = subprocess.PIPE
1671 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1672 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001673 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001674 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001675 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001676 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001677 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001678 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001679 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1680 if code != 0:
1681 return cmd_data.message(
1682 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1683 if cmd_data.info:
1684 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001685
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001686
sbc@chromium.org013731e2015-02-26 18:28:43 +00001687def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001688 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001689 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001690 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001691 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001692 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1693 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001694 parser.add_option("-r", "--recursive", action="store_true",
1695 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001696 parser.add_option("-v", "--verbose", action="count", default=0,
1697 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001698 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001699 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001700 parser.add_option("--description", default='')
1701 parser.add_option("--issue", type='int', default=0)
1702 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001703 parser.add_option("--root", default=os.getcwd(),
1704 help="Search for PRESUBMIT.py up to this directory. "
1705 "If inherit-review-settings-ok is present in this "
1706 "directory, parent directories up to the root file "
1707 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001708 parser.add_option("--upstream",
1709 help="Git only: the base ref or upstream branch against "
1710 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001711 parser.add_option("--default_presubmit")
1712 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001713 parser.add_option("--skip_canned", action='append', default=[],
1714 help="A list of checks to skip which appear in "
1715 "presubmit_canned_checks. Can be provided multiple times "
1716 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001717 parser.add_option("--dry_run", action='store_true',
1718 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001719 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001720 parser.add_option("--gerrit_fetch", action='store_true',
1721 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001722 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1723 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001724 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1725 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001726 # These are for OAuth2 authentication for bots. See also apply_issue.py
1727 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1728 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1729
phajdan.jr@chromium.org2ff30182016-03-23 09:52:51 +00001730 # TODO(phajdan.jr): Update callers and remove obsolete --trybot-json .
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001731 parser.add_option("--trybot-json",
1732 help="Output trybot information to the file specified.")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001733 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001734 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001735 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001736
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001737 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001738 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001739 elif options.verbose:
1740 logging.basicConfig(level=logging.INFO)
1741 else:
1742 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001743
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001744 if (any((options.rietveld_url, options.rietveld_email_file,
1745 options.rietveld_fetch, options.rietveld_private_key_file))
1746 and any((options.gerrit_url, options.gerrit_fetch))):
1747 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1748 'allowed')
1749
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001750 if options.rietveld_email and options.rietveld_email_file:
1751 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1752 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001753 if options.rietveld_email_file:
1754 with open(options.rietveld_email_file, "rb") as f:
1755 options.rietveld_email = f.read().strip()
1756
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001757 change_class, files = load_files(options, args)
1758 if not change_class:
1759 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001760 logging.info('Found %d file(s).' % len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001761
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001762 rietveld_obj, gerrit_obj = None, None
1763
maruel@chromium.org239f4112011-06-03 20:08:23 +00001764 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001765 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001766 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001767 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1768 options.rietveld_url,
1769 options.rietveld_email,
1770 options.rietveld_private_key_file)
1771 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001772 rietveld_obj = rietveld.CachingRietveld(
1773 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001774 auth_config,
1775 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001776 if options.rietveld_fetch:
1777 assert options.issue
1778 props = rietveld_obj.get_issue_properties(options.issue, False)
1779 options.author = props['owner_email']
1780 options.description = props['description']
1781 logging.info('Got author: "%s"', options.author)
1782 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001783
1784 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001785 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001786 rietveld_obj = None
1787 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1788 options.author = gerrit_obj.GetChangeOwner(options.issue)
1789 options.description = gerrit_obj.GetChangeDescription(options.issue,
1790 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001791 logging.info('Got author: "%s"', options.author)
1792 logging.info('Got description: """\n%s\n"""', options.description)
1793
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001794 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001795 with canned_check_filter(options.skip_canned):
1796 results = DoPresubmitChecks(
1797 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001798 options.description,
1799 options.root,
1800 files,
1801 options.issue,
1802 options.patchset,
1803 options.author,
1804 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001805 options.commit,
1806 options.verbose,
1807 sys.stdout,
1808 sys.stdin,
1809 options.default_presubmit,
1810 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001811 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001812 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001813 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001814 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001815 except NonexistantCannedCheckFilter, e:
1816 print >> sys.stderr, (
1817 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1818 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001819 except PresubmitFailure, e:
1820 print >> sys.stderr, e
1821 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1822 print >> sys.stderr, 'If all fails, contact maruel@'
1823 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001824
1825
1826if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001827 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001828 try:
1829 sys.exit(main())
1830 except KeyboardInterrupt:
1831 sys.stderr.write('interrupted\n')
1832 sys.exit(1)