blob: 1f97d7f1c2baa6bb615392981bc17afd333d8985 [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.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100200 try:
201 return gerrit_util.GetChangeDetail(
202 self.host, str(issue),
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100203 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'],
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100204 ignore_404=False)
205 except gerrit_util.GerritError as e:
206 if e.http_status == 404:
207 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
208 'no credentials to fetch issue details' % issue)
209 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000210
211 def GetChangeInfo(self, issue):
212 """Returns labels and all revisions (patchsets) for this issue.
213
214 The result is a dictionary according to Gerrit REST Api.
215 https://gerrit-review.googlesource.com/Documentation/rest-api.html
216
217 However, API isn't very clear what's inside, so see tests for example.
218 """
219 assert issue
220 cache_key = int(issue)
221 if cache_key not in self.cache:
222 self.cache[cache_key] = self._FetchChangeDetail(issue)
223 return self.cache[cache_key]
224
225 def GetChangeDescription(self, issue, patchset=None):
226 """If patchset is none, fetches current patchset."""
227 info = self.GetChangeInfo(issue)
228 # info is a reference to cache. We'll modify it here adding description to
229 # it to the right patchset, if it is not yet there.
230
231 # Find revision info for the patchset we want.
232 if patchset is not None:
233 for rev, rev_info in info['revisions'].iteritems():
234 if str(rev_info['_number']) == str(patchset):
235 break
236 else:
237 raise Exception('patchset %s doesn\'t exist in issue %s' % (
238 patchset, issue))
239 else:
240 rev = info['current_revision']
241 rev_info = info['revisions'][rev]
242
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100243 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000244
245 def GetChangeOwner(self, issue):
246 return self.GetChangeInfo(issue)['owner']['email']
247
248 def GetChangeReviewers(self, issue, approving_only=True):
agable565adb52016-07-22 14:48:07 -0700249 cr = self.GetChangeInfo(issue)['labels']['Code-Review']
250 max_value = max(int(k) for k in cr['values'].keys())
Aaron Gablef5644a92016-12-02 15:31:58 -0800251 return [r.get('email') for r in cr.get('all', [])
agable565adb52016-07-22 14:48:07 -0700252 if not approving_only or r.get('value', 0) == max_value]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000253
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000254
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000255class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000256 """An instance of OutputApi gets passed to presubmit scripts so that they
257 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000258 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000259 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000260 PresubmitError = _PresubmitError
261 PresubmitPromptWarning = _PresubmitPromptWarning
262 PresubmitNotifyResult = _PresubmitNotifyResult
263 MailTextResult = _MailTextResult
264
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000265 def __init__(self, is_committing):
266 self.is_committing = is_committing
267
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000268 def PresubmitPromptOrNotify(self, *args, **kwargs):
269 """Warn the user when uploading, but only notify if committing."""
270 if self.is_committing:
271 return self.PresubmitNotifyResult(*args, **kwargs)
272 return self.PresubmitPromptWarning(*args, **kwargs)
273
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000274
275class InputApi(object):
276 """An instance of this object is passed to presubmit scripts so they can
277 know stuff about the change they're looking at.
278 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000279 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800280 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000281
maruel@chromium.org3410d912009-06-09 20:56:16 +0000282 # File extensions that are considered source files from a style guide
283 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000284 #
285 # Files without an extension aren't included in the list. If you want to
286 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
287 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000288 DEFAULT_WHITE_LIST = (
289 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000290 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
291 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000292 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000293 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000294 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000295 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000296 )
297
298 # Path regexp that should be excluded from being considered containing source
299 # files. Don't modify this list from a presubmit script!
300 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000301 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000302 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000303 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
304 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000305 # Output directories (just in case)
306 r".*\bDebug[\\\/].*",
307 r".*\bRelease[\\\/].*",
308 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000309 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000310 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000311 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000312 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000313 r"(|.*[\\\/])\.git[\\\/].*",
314 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000315 # There is no point in processing a patch file.
316 r".+\.diff$",
317 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000318 )
319
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000320 def __init__(self, change, presubmit_path, is_committing,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000321 rietveld_obj, verbose, gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000322 """Builds an InputApi object.
323
324 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000325 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000326 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000327 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000328 rietveld_obj: rietveld.Rietveld client object
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000329 gerrit_obj: provides basic Gerrit codereview functionality.
330 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000331 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000332 # Version number of the presubmit_support script.
333 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000334 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000335 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000336 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000337 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000338 self.dry_run = dry_run
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000339 # TBD
340 self.host_url = 'http://codereview.chromium.org'
341 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000342 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000343
344 # We expose various modules and functions as attributes of the input_api
345 # so that presubmit scripts don't have to import them.
346 self.basename = os.path.basename
347 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000348 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000349 self.cStringIO = cStringIO
dcheng091b7db2016-06-16 01:27:51 -0700350 self.fnmatch = fnmatch
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000351 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000352 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000353 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000354 self.os_listdir = os.listdir
355 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000356 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000357 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000358 self.pickle = pickle
359 self.marshal = marshal
360 self.re = re
361 self.subprocess = subprocess
362 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000363 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000364 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000365 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000366 self.urllib2 = urllib2
367
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000368 # To easily fork python.
369 self.python_executable = sys.executable
370 self.environ = os.environ
371
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000372 # InputApi.platform is the platform you're currently running on.
373 self.platform = sys.platform
374
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000375 self.cpu_count = multiprocessing.cpu_count()
376
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000377 # this is done here because in RunTests, the current working directory has
378 # changed, which causes Pool() to explode fantastically when run on windows
379 # (because it tries to load the __main__ module, which imports lots of
380 # things relative to the current working directory).
381 self._run_tests_pool = multiprocessing.Pool(self.cpu_count)
382
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000383 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000384 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000385
386 # We carry the canned checks so presubmit scripts can easily use them.
387 self.canned_checks = presubmit_canned_checks
388
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000389 # TODO(dpranke): figure out a list of all approved owners for a repo
390 # in order to be able to handle wildcard OWNERS files?
391 self.owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -0700392 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000393 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000394 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000395
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000396 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000397 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000398 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800399 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000400 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000401 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000402 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
403 for (a, b, header) in cpplint._re_pattern_templates
404 ]
405
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000406 def PresubmitLocalPath(self):
407 """Returns the local path of the presubmit script currently being run.
408
409 This is useful if you don't want to hard-code absolute paths in the
410 presubmit script. For example, It can be used to find another file
411 relative to the PRESUBMIT.py script, so the whole tree can be branched and
412 the presubmit script still works, without editing its content.
413 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000414 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000415
agable0b65e732016-11-22 09:25:46 -0800416 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000417 """Same as input_api.change.AffectedFiles() except only lists files
418 (and optionally directories) in the same directory as the current presubmit
419 script, or subdirectories thereof.
420 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000421 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000422 if len(dir_with_slash) == 1:
423 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000424
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000425 return filter(
426 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
agable0b65e732016-11-22 09:25:46 -0800427 self.change.AffectedFiles(include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000428
agable0b65e732016-11-22 09:25:46 -0800429 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000430 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800431 paths = [af.LocalPath() for af in self.AffectedFiles()]
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000432 logging.debug("LocalPaths: %s", paths)
433 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000434
agable0b65e732016-11-22 09:25:46 -0800435 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000436 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800437 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000438
agable0b65e732016-11-22 09:25:46 -0800439 def AffectedTestableFiles(self, include_deletes=None):
440 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000441 in the same directory as the current presubmit script, or subdirectories
442 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000443 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000444 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800445 warn("AffectedTestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000446 " is deprecated and ignored" % str(include_deletes),
447 category=DeprecationWarning,
448 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800449 return filter(lambda x: x.IsTestableFile(),
450 self.AffectedFiles(include_deletes=False))
451
452 def AffectedTextFiles(self, include_deletes=None):
453 """An alias to AffectedTestableFiles for backwards compatibility."""
454 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000455
maruel@chromium.org3410d912009-06-09 20:56:16 +0000456 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
457 """Filters out files that aren't considered "source file".
458
459 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
460 and InputApi.DEFAULT_BLACK_LIST is used respectively.
461
462 The lists will be compiled as regular expression and
463 AffectedFile.LocalPath() needs to pass both list.
464
465 Note: Copy-paste this function to suit your needs or use a lambda function.
466 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000467 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000468 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000469 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000470 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000471 return True
472 return False
473 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
474 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
475
476 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800477 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000478
479 If source_file is None, InputApi.FilterSourceFile() is used.
480 """
481 if not source_file:
482 source_file = self.FilterSourceFile
agable0b65e732016-11-22 09:25:46 -0800483 return filter(source_file, self.AffectedTestableFiles())
maruel@chromium.org3410d912009-06-09 20:56:16 +0000484
485 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000486 """An iterator over all text lines in "new" version of changed files.
487
488 Only lists lines from new or modified text files in the change that are
489 contained by the directory of the currently executing presubmit script.
490
491 This is useful for doing line-by-line regex checks, like checking for
492 trailing whitespace.
493
494 Yields:
495 a 3 tuple:
496 the AffectedFile instance of the current file;
497 integer line number (1-based); and
498 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000499
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000500 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000501 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000502 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000503 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000504
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000505 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000506 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000507
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000508 Deny reading anything outside the repository.
509 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000510 if isinstance(file_item, AffectedFile):
511 file_item = file_item.AbsoluteLocalPath()
512 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000513 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000514 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000515
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000516 @property
517 def tbr(self):
518 """Returns if a change is TBR'ed."""
519 return 'TBR' in self.change.tags
520
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000521 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000522 tests = []
523 msgs = []
524 for t in tests_mix:
525 if isinstance(t, OutputApi.PresubmitResult):
526 msgs.append(t)
527 else:
528 assert issubclass(t.message, _PresubmitResult)
529 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000530 if self.verbose:
531 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000532 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000533 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000534 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000535 else:
536 msgs.extend(map(CallCommand, tests))
537 return [m for m in msgs if m]
538
scottmg86099d72016-09-01 09:16:51 -0700539 def ShutdownPool(self):
540 self._run_tests_pool.close()
541 self._run_tests_pool.join()
542 self._run_tests_pool = None
543
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000544
nick@chromium.orgff526192013-06-10 19:30:26 +0000545class _DiffCache(object):
546 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000547 def __init__(self, upstream=None):
548 """Stores the upstream revision against which all diffs will be computed."""
549 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000550
551 def GetDiff(self, path, local_root):
552 """Get the diff for a particular path."""
553 raise NotImplementedError()
554
555
nick@chromium.orgff526192013-06-10 19:30:26 +0000556class _GitDiffCache(_DiffCache):
557 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000558 def __init__(self, upstream):
559 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000560 self._diffs_by_file = None
561
562 def GetDiff(self, path, local_root):
563 if not self._diffs_by_file:
564 # Compute a single diff for all files and parse the output; should
565 # with git this is much faster than computing one diff for each file.
566 diffs = {}
567
568 # Don't specify any filenames below, because there are command line length
569 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000570 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
571 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000572
573 # This regex matches the path twice, separated by a space. Note that
574 # filename itself may contain spaces.
575 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
576 current_diff = []
577 keep_line_endings = True
578 for x in unified_diff.splitlines(keep_line_endings):
579 match = file_marker.match(x)
580 if match:
581 # Marks the start of a new per-file section.
582 diffs[match.group('filename')] = current_diff = [x]
583 elif x.startswith('diff --git'):
584 raise PresubmitFailure('Unexpected diff line: %s' % x)
585 else:
586 current_diff.append(x)
587
588 self._diffs_by_file = dict(
589 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
590
591 if path not in self._diffs_by_file:
592 raise PresubmitFailure(
593 'Unified diff did not contain entry for file %s' % path)
594
595 return self._diffs_by_file[path]
596
597
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000598class AffectedFile(object):
599 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000600
601 DIFF_CACHE = _DiffCache
602
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000603 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800604 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000605 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000606 self._path = path
607 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000608 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000609 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000610 self._cached_changed_contents = None
611 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000612 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700613 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000614
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000615 def LocalPath(self):
616 """Returns the path of this file on the local disk relative to client root.
617 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000618 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000619
620 def AbsoluteLocalPath(self):
621 """Returns the absolute path of this file on the local disk.
622 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000623 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000624
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000625 def Action(self):
626 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000627 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
628 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000629 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000630
agable0b65e732016-11-22 09:25:46 -0800631 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000632 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000633
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000634 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000635 raise NotImplementedError() # Implement when needed
636
agable0b65e732016-11-22 09:25:46 -0800637 def IsTextFile(self):
638 """An alias to IsTestableFile for backwards compatibility."""
639 return self.IsTestableFile()
640
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000641 def NewContents(self):
642 """Returns an iterator over the lines in the new version of file.
643
644 The new version is the file in the user's workspace, i.e. the "right hand
645 side".
646
647 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000648 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000649 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000650 if self._cached_new_contents is None:
651 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800652 try:
653 self._cached_new_contents = gclient_utils.FileRead(
654 self.AbsoluteLocalPath(), 'rU').splitlines()
655 except IOError:
656 pass # File not found? That's fine; maybe it was deleted.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000657 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000658
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000659 def ChangedContents(self):
660 """Returns a list of tuples (line number, line text) of all new lines.
661
662 This relies on the scm diff output describing each changed code section
663 with a line of the form
664
665 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
666 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000667 if self._cached_changed_contents is not None:
668 return self._cached_changed_contents[:]
669 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000670 line_num = 0
671
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000672 for line in self.GenerateScmDiff().splitlines():
673 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
674 if m:
675 line_num = int(m.groups(1)[0])
676 continue
677 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000678 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000679 if not line.startswith('-'):
680 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000681 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000682
maruel@chromium.org5de13972009-06-10 18:16:06 +0000683 def __str__(self):
684 return self.LocalPath()
685
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000686 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000687 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000688
maruel@chromium.org58407af2011-04-12 23:15:57 +0000689
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000690class GitAffectedFile(AffectedFile):
691 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000692 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800693 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000694
nick@chromium.orgff526192013-06-10 19:30:26 +0000695 DIFF_CACHE = _GitDiffCache
696
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000697 def __init__(self, *args, **kwargs):
698 AffectedFile.__init__(self, *args, **kwargs)
699 self._server_path = None
agable0b65e732016-11-22 09:25:46 -0800700 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000701
agable0b65e732016-11-22 09:25:46 -0800702 def IsTestableFile(self):
703 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000704 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -0800705 # A deleted file is not testable.
706 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000707 else:
agable0b65e732016-11-22 09:25:46 -0800708 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
709 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000710
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000711
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000712class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000713 """Describe a change.
714
715 Used directly by the presubmit scripts to query the current change being
716 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000717
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000718 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000719 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000720 self.KEY: equivalent to tags['KEY']
721 """
722
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000723 _AFFECTED_FILES = AffectedFile
724
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000725 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000726 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000727 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000728 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000729
maruel@chromium.org58407af2011-04-12 23:15:57 +0000730 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000731 self, name, description, local_root, files, issue, patchset, author,
732 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000733 if files is None:
734 files = []
735 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000736 # Convert root into an absolute path.
737 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000738 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000739 self.issue = issue
740 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000741 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000743 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000744 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000745 self._description_without_tags = ''
746 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000747
maruel@chromium.orge085d812011-10-10 19:49:15 +0000748 assert all(
749 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
750
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000751 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000752 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000753 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
754 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000755 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000756
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000757 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000758 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000759 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000760
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000761 def DescriptionText(self):
762 """Returns the user-entered changelist description, minus tags.
763
764 Any line in the user-provided description starting with e.g. "FOO="
765 (whitespace permitted before and around) is considered a tag line. Such
766 lines are stripped out of the description this function returns.
767 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000768 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000769
770 def FullDescriptionText(self):
771 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000772 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000773
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000774 def SetDescriptionText(self, description):
775 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000776
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000777 Also updates the list of tags."""
778 self._full_description = description
779
780 # From the description text, build up a dictionary of key/value pairs
781 # plus the description minus all key/value or "tag" lines.
782 description_without_tags = []
783 self.tags = {}
784 for line in self._full_description.splitlines():
785 m = self.TAG_LINE_RE.match(line)
786 if m:
787 self.tags[m.group('key')] = m.group('value')
788 else:
789 description_without_tags.append(line)
790
791 # Change back to text and remove whitespace at end.
792 self._description_without_tags = (
793 '\n'.join(description_without_tags).rstrip())
794
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000795 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000796 """Returns the repository (checkout) root directory for this change,
797 as an absolute path.
798 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000799 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000800
801 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000802 """Return tags directly as attributes on the object."""
803 if not re.match(r"^[A-Z_]*$", attr):
804 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000805 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000806
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000807 def AllFiles(self, root=None):
808 """List all files under source control in the repo."""
809 raise NotImplementedError()
810
agable0b65e732016-11-22 09:25:46 -0800811 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000812 """Returns a list of AffectedFile instances for all files in the change.
813
814 Args:
815 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +0000816 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000817
818 Returns:
819 [AffectedFile(path, action), AffectedFile(path, action)]
820 """
agable0b65e732016-11-22 09:25:46 -0800821 affected = filter(file_filter, self._affected_files)
sail@chromium.org5538e022011-05-12 17:53:16 +0000822
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000823 if include_deletes:
824 return affected
825 else:
826 return filter(lambda x: x.Action() != 'D', affected)
827
agable0b65e732016-11-22 09:25:46 -0800828 def AffectedTestableFiles(self, include_deletes=None):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000829 """Return a list of the existing text files in a change."""
830 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800831 warn("AffectedTeestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000832 " is deprecated and ignored" % str(include_deletes),
833 category=DeprecationWarning,
834 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800835 return filter(lambda x: x.IsTestableFile(),
836 self.AffectedFiles(include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000837
agable0b65e732016-11-22 09:25:46 -0800838 def AffectedTextFiles(self, include_deletes=None):
839 """An alias to AffectedTestableFiles for backwards compatibility."""
840 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000841
agable0b65e732016-11-22 09:25:46 -0800842 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000843 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800844 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000845
agable0b65e732016-11-22 09:25:46 -0800846 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000847 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800848 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000849
850 def RightHandSideLines(self):
851 """An iterator over all text lines in "new" version of changed files.
852
853 Lists lines from new or modified text files in the change.
854
855 This is useful for doing line-by-line regex checks, like checking for
856 trailing whitespace.
857
858 Yields:
859 a 3 tuple:
860 the AffectedFile instance of the current file;
861 integer line number (1-based); and
862 the contents of the line as a string.
863 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000864 return _RightHandSideLinesImpl(
865 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -0800866 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000867
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000868
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000869class GitChange(Change):
870 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000871 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000872
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000873 def AllFiles(self, root=None):
874 """List all files under source control in the repo."""
875 root = root or self.RepositoryRoot()
876 return subprocess.check_output(
877 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
878
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000879
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000880def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000881 """Finds all presubmit files that apply to a given set of source files.
882
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000883 If inherit-review-settings-ok is present right under root, looks for
884 PRESUBMIT.py in directories enclosing root.
885
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000886 Args:
887 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000888 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000889
890 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000891 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000892 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000893 files = [normpath(os.path.join(root, f)) for f in files]
894
895 # List all the individual directories containing files.
896 directories = set([os.path.dirname(f) for f in files])
897
898 # Ignore root if inherit-review-settings-ok is present.
899 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
900 root = None
901
902 # Collect all unique directories that may contain PRESUBMIT.py.
903 candidates = set()
904 for directory in directories:
905 while True:
906 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000907 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000908 candidates.add(directory)
909 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000910 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000911 parent_dir = os.path.dirname(directory)
912 if parent_dir == directory:
913 # We hit the system root directory.
914 break
915 directory = parent_dir
916
917 # Look for PRESUBMIT.py in all candidate directories.
918 results = []
919 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -0700920 try:
921 for f in os.listdir(directory):
922 p = os.path.join(directory, f)
923 if os.path.isfile(p) and re.match(
924 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
925 results.append(p)
926 except OSError:
927 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000928
tobiasjs2836bcf2016-08-16 04:08:16 -0700929 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000930 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000931
932
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +0000933class GetTryMastersExecuter(object):
934 @staticmethod
935 def ExecPresubmitScript(script_text, presubmit_path, project, change):
936 """Executes GetPreferredTryMasters() from a single presubmit script.
937
938 Args:
939 script_text: The text of the presubmit script.
940 presubmit_path: Project script to run.
941 project: Project name to pass to presubmit script for bot selection.
942
943 Return:
944 A map of try masters to map of builders to set of tests.
945 """
946 context = {}
947 try:
948 exec script_text in context
949 except Exception, e:
950 raise PresubmitFailure('"%s" had an exception.\n%s'
951 % (presubmit_path, e))
952
953 function_name = 'GetPreferredTryMasters'
954 if function_name not in context:
955 return {}
956 get_preferred_try_masters = context[function_name]
957 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
958 raise PresubmitFailure(
959 'Expected function "GetPreferredTryMasters" to take two arguments.')
960 return get_preferred_try_masters(project, change)
961
962
rmistry@google.com5626a922015-02-26 14:03:30 +0000963class GetPostUploadExecuter(object):
964 @staticmethod
965 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
966 """Executes PostUploadHook() from a single presubmit script.
967
968 Args:
969 script_text: The text of the presubmit script.
970 presubmit_path: Project script to run.
971 cl: The Changelist object.
972 change: The Change object.
973
974 Return:
975 A list of results objects.
976 """
977 context = {}
978 try:
979 exec script_text in context
980 except Exception, e:
981 raise PresubmitFailure('"%s" had an exception.\n%s'
982 % (presubmit_path, e))
983
984 function_name = 'PostUploadHook'
985 if function_name not in context:
986 return {}
987 post_upload_hook = context[function_name]
988 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
989 raise PresubmitFailure(
990 'Expected function "PostUploadHook" to take three arguments.')
991 return post_upload_hook(cl, change, OutputApi(False))
992
993
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +0000994def _MergeMasters(masters1, masters2):
995 """Merges two master maps. Merges also the tests of each builder."""
996 result = {}
997 for (master, builders) in itertools.chain(masters1.iteritems(),
998 masters2.iteritems()):
999 new_builders = result.setdefault(master, {})
1000 for (builder, tests) in builders.iteritems():
1001 new_builders.setdefault(builder, set([])).update(tests)
1002 return result
1003
1004
1005def DoGetTryMasters(change,
1006 changed_files,
1007 repository_root,
1008 default_presubmit,
1009 project,
1010 verbose,
1011 output_stream):
1012 """Get the list of try masters from the presubmit scripts.
1013
1014 Args:
1015 changed_files: List of modified files.
1016 repository_root: The repository root.
1017 default_presubmit: A default presubmit script to execute in any case.
1018 project: Optional name of a project used in selecting trybots.
1019 verbose: Prints debug info.
1020 output_stream: A stream to write debug output to.
1021
1022 Return:
1023 Map of try masters to map of builders to set of tests.
1024 """
1025 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1026 if not presubmit_files and verbose:
1027 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1028 results = {}
1029 executer = GetTryMastersExecuter()
1030
1031 if default_presubmit:
1032 if verbose:
1033 output_stream.write("Running default presubmit script.\n")
1034 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1035 results = _MergeMasters(results, executer.ExecPresubmitScript(
1036 default_presubmit, fake_path, project, change))
1037 for filename in presubmit_files:
1038 filename = os.path.abspath(filename)
1039 if verbose:
1040 output_stream.write("Running %s\n" % filename)
1041 # Accept CRLF presubmit script.
1042 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1043 results = _MergeMasters(results, executer.ExecPresubmitScript(
1044 presubmit_script, filename, project, change))
1045
1046 # Make sets to lists again for later JSON serialization.
1047 for builders in results.itervalues():
1048 for builder in builders:
1049 builders[builder] = list(builders[builder])
1050
1051 if results and verbose:
1052 output_stream.write('%s\n' % str(results))
1053 return results
1054
1055
rmistry@google.com5626a922015-02-26 14:03:30 +00001056def DoPostUploadExecuter(change,
1057 cl,
1058 repository_root,
1059 verbose,
1060 output_stream):
1061 """Execute the post upload hook.
1062
1063 Args:
1064 change: The Change object.
1065 cl: The Changelist object.
1066 repository_root: The repository root.
1067 verbose: Prints debug info.
1068 output_stream: A stream to write debug output to.
1069 """
1070 presubmit_files = ListRelevantPresubmitFiles(
1071 change.LocalPaths(), repository_root)
1072 if not presubmit_files and verbose:
1073 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1074 results = []
1075 executer = GetPostUploadExecuter()
1076 # The root presubmit file should be executed after the ones in subdirectories.
1077 # i.e. the specific post upload hooks should run before the general ones.
1078 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1079 presubmit_files.reverse()
1080
1081 for filename in presubmit_files:
1082 filename = os.path.abspath(filename)
1083 if verbose:
1084 output_stream.write("Running %s\n" % filename)
1085 # Accept CRLF presubmit script.
1086 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1087 results.extend(executer.ExecPresubmitScript(
1088 presubmit_script, filename, cl, change))
1089 output_stream.write('\n')
1090 if results:
1091 output_stream.write('** Post Upload Hook Messages **\n')
1092 for result in results:
1093 result.handle(output_stream)
1094 output_stream.write('\n')
1095
1096 return results
1097
1098
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001099class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001100 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001101 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001102 """
1103 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001104 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001105 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001106 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001107 gerrit_obj: provides basic Gerrit codereview functionality.
1108 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001109 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001110 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001111 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001112 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001113 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001114 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001115 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001116
1117 def ExecPresubmitScript(self, script_text, presubmit_path):
1118 """Executes a single presubmit script.
1119
1120 Args:
1121 script_text: The text of the presubmit script.
1122 presubmit_path: The path to the presubmit file (this will be reported via
1123 input_api.PresubmitLocalPath()).
1124
1125 Return:
1126 A list of result objects, empty if no problems.
1127 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001128
chase@chromium.org8e416c82009-10-06 04:30:44 +00001129 # Change to the presubmit file's directory to support local imports.
1130 main_path = os.getcwd()
1131 os.chdir(os.path.dirname(presubmit_path))
1132
1133 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001134 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001135 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001136 gerrit_obj=self.gerrit, dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001137 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001138 try:
1139 exec script_text in context
1140 except Exception, e:
1141 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001142
1143 # These function names must change if we make substantial changes to
1144 # the presubmit API that are not backwards compatible.
1145 if self.committing:
1146 function_name = 'CheckChangeOnCommit'
1147 else:
1148 function_name = 'CheckChangeOnUpload'
1149 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001150 context['__args'] = (input_api, OutputApi(self.committing))
tobiasjs2836bcf2016-08-16 04:08:16 -07001151 logging.debug('Running %s in %s', function_name, presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001152 result = eval(function_name + '(*__args)', context)
tobiasjs2836bcf2016-08-16 04:08:16 -07001153 logging.debug('Running %s done.', function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001154 if not (isinstance(result, types.TupleType) or
1155 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001156 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001157 'Presubmit functions must return a tuple or list')
1158 for item in result:
1159 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001160 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001161 'All presubmit results must be of types derived from '
1162 'output_api.PresubmitResult')
1163 else:
1164 result = () # no error since the script doesn't care about current event.
1165
scottmg86099d72016-09-01 09:16:51 -07001166 input_api.ShutdownPool()
1167
chase@chromium.org8e416c82009-10-06 04:30:44 +00001168 # Return the process to the original working directory.
1169 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001170 return result
1171
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001172def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001173 committing,
1174 verbose,
1175 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001176 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001177 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001178 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001179 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001180 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001181 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001182 """Runs all presubmit checks that apply to the files in the change.
1183
1184 This finds all PRESUBMIT.py files in directories enclosing the files in the
1185 change (up to the repository root) and calls the relevant entrypoint function
1186 depending on whether the change is being committed or uploaded.
1187
1188 Prints errors, warnings and notifications. Prompts the user for warnings
1189 when needed.
1190
1191 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001192 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001193 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001194 verbose: Prints debug info.
1195 output_stream: A stream to write output from presubmit tests to.
1196 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001197 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001198 may_prompt: Enable (y/n) questions on warning or error. If False,
1199 any questions are answered with yes by default.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001200 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001201 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001202 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001203
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001204 Warning:
1205 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1206 SHOULD be sys.stdin.
1207
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001208 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001209 A PresubmitOutput object. Use output.should_continue() to figure out
1210 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001211 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001212 old_environ = os.environ
1213 try:
1214 # Make sure python subprocesses won't generate .pyc files.
1215 os.environ = os.environ.copy()
1216 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001217
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001218 output = PresubmitOutput(input_stream, output_stream)
1219 if committing:
1220 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001221 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001222 output.write("Running presubmit upload checks ...\n")
1223 start_time = time.time()
1224 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001225 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001226 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001227 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001228 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001229 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001230 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001231 if default_presubmit:
1232 if verbose:
1233 output.write("Running default presubmit script.\n")
1234 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1235 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1236 for filename in presubmit_files:
1237 filename = os.path.abspath(filename)
1238 if verbose:
1239 output.write("Running %s\n" % filename)
1240 # Accept CRLF presubmit script.
1241 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1242 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001243
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001244 errors = []
1245 notifications = []
1246 warnings = []
1247 for result in results:
1248 if result.fatal:
1249 errors.append(result)
1250 elif result.should_prompt:
1251 warnings.append(result)
1252 else:
1253 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001254
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001255 output.write('\n')
1256 for name, items in (('Messages', notifications),
1257 ('Warnings', warnings),
1258 ('ERRORS', errors)):
1259 if items:
1260 output.write('** Presubmit %s **\n' % name)
1261 for item in items:
1262 item.handle(output)
1263 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001264
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001265 total_time = time.time() - start_time
1266 if total_time > 1.0:
1267 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001268
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001269 if errors:
1270 output.fail()
1271 elif warnings:
1272 output.write('There were presubmit warnings. ')
1273 if may_prompt:
1274 output.prompt_yes_no('Are you sure you wish to continue? (y/N): ')
1275 else:
1276 output.write('Presubmit checks passed.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001277
1278 global _ASKED_FOR_FEEDBACK
1279 # Ask for feedback one time out of 5.
1280 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001281 output.write(
1282 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1283 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1284 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001285 _ASKED_FOR_FEEDBACK = True
1286 return output
1287 finally:
1288 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001289
1290
1291def ScanSubDirs(mask, recursive):
1292 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001293 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001294 else:
1295 results = []
1296 for root, dirs, files in os.walk('.'):
1297 if '.svn' in dirs:
1298 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001299 if '.git' in dirs:
1300 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001301 for name in files:
1302 if fnmatch.fnmatch(name, mask):
1303 results.append(os.path.join(root, name))
1304 return results
1305
1306
1307def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001308 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001309 files = []
1310 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001311 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001312 return files
1313
1314
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001315def load_files(options, args):
1316 """Tries to determine the SCM."""
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001317 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001318 if args:
1319 files = ParseFiles(args, options.recursive)
agable0b65e732016-11-22 09:25:46 -08001320 change_scm = scm.determine_scm(options.root)
1321 if change_scm == 'git':
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001322 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001323 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001324 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001325 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001326 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001327 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001328 if not files:
1329 return None, None
1330 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001331 return change_class, files
1332
1333
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001334class NonexistantCannedCheckFilter(Exception):
1335 pass
1336
1337
1338@contextlib.contextmanager
1339def canned_check_filter(method_names):
1340 filtered = {}
1341 try:
1342 for method_name in method_names:
1343 if not hasattr(presubmit_canned_checks, method_name):
1344 raise NonexistantCannedCheckFilter(method_name)
1345 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1346 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1347 yield
1348 finally:
1349 for name, method in filtered.iteritems():
1350 setattr(presubmit_canned_checks, name, method)
1351
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001352
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001353def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001354 """Runs an external program, potentially from a child process created by the
1355 multiprocessing module.
1356
1357 multiprocessing needs a top level function with a single argument.
1358 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001359 cmd_data.kwargs['stdout'] = subprocess.PIPE
1360 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1361 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001362 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001363 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001364 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001365 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001366 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001367 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001368 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1369 if code != 0:
1370 return cmd_data.message(
1371 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1372 if cmd_data.info:
1373 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001374
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001375
sbc@chromium.org013731e2015-02-26 18:28:43 +00001376def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001377 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001378 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001379 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001380 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001381 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1382 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001383 parser.add_option("-r", "--recursive", action="store_true",
1384 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001385 parser.add_option("-v", "--verbose", action="count", default=0,
1386 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001387 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001388 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001389 parser.add_option("--description", default='')
1390 parser.add_option("--issue", type='int', default=0)
1391 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001392 parser.add_option("--root", default=os.getcwd(),
1393 help="Search for PRESUBMIT.py up to this directory. "
1394 "If inherit-review-settings-ok is present in this "
1395 "directory, parent directories up to the root file "
1396 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001397 parser.add_option("--upstream",
1398 help="Git only: the base ref or upstream branch against "
1399 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001400 parser.add_option("--default_presubmit")
1401 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001402 parser.add_option("--skip_canned", action='append', default=[],
1403 help="A list of checks to skip which appear in "
1404 "presubmit_canned_checks. Can be provided multiple times "
1405 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001406 parser.add_option("--dry_run", action='store_true',
1407 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001408 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001409 parser.add_option("--gerrit_fetch", action='store_true',
1410 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001411 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1412 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001413 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1414 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001415 # These are for OAuth2 authentication for bots. See also apply_issue.py
1416 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1417 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1418
phajdan.jr@chromium.org2ff30182016-03-23 09:52:51 +00001419 # TODO(phajdan.jr): Update callers and remove obsolete --trybot-json .
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001420 parser.add_option("--trybot-json",
1421 help="Output trybot information to the file specified.")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001422 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001423 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001424 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001425
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001426 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001427 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001428 elif options.verbose:
1429 logging.basicConfig(level=logging.INFO)
1430 else:
1431 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001432
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001433 if (any((options.rietveld_url, options.rietveld_email_file,
1434 options.rietveld_fetch, options.rietveld_private_key_file))
1435 and any((options.gerrit_url, options.gerrit_fetch))):
1436 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1437 'allowed')
1438
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001439 if options.rietveld_email and options.rietveld_email_file:
1440 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1441 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001442 if options.rietveld_email_file:
1443 with open(options.rietveld_email_file, "rb") as f:
1444 options.rietveld_email = f.read().strip()
1445
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001446 change_class, files = load_files(options, args)
1447 if not change_class:
1448 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001449 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001450
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001451 rietveld_obj, gerrit_obj = None, None
1452
maruel@chromium.org239f4112011-06-03 20:08:23 +00001453 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001454 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001455 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001456 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1457 options.rietveld_url,
1458 options.rietveld_email,
1459 options.rietveld_private_key_file)
1460 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001461 rietveld_obj = rietveld.CachingRietveld(
1462 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001463 auth_config,
1464 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001465 if options.rietveld_fetch:
1466 assert options.issue
1467 props = rietveld_obj.get_issue_properties(options.issue, False)
1468 options.author = props['owner_email']
1469 options.description = props['description']
1470 logging.info('Got author: "%s"', options.author)
1471 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001472
1473 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001474 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001475 rietveld_obj = None
1476 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1477 options.author = gerrit_obj.GetChangeOwner(options.issue)
1478 options.description = gerrit_obj.GetChangeDescription(options.issue,
1479 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001480 logging.info('Got author: "%s"', options.author)
1481 logging.info('Got description: """\n%s\n"""', options.description)
1482
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001483 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001484 with canned_check_filter(options.skip_canned):
1485 results = DoPresubmitChecks(
1486 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001487 options.description,
1488 options.root,
1489 files,
1490 options.issue,
1491 options.patchset,
1492 options.author,
1493 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001494 options.commit,
1495 options.verbose,
1496 sys.stdout,
1497 sys.stdin,
1498 options.default_presubmit,
1499 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001500 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001501 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001502 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001503 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001504 except NonexistantCannedCheckFilter, e:
1505 print >> sys.stderr, (
1506 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1507 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001508 except PresubmitFailure, e:
1509 print >> sys.stderr, e
1510 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1511 print >> sys.stderr, 'If all fails, contact maruel@'
1512 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001513
1514
1515if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001516 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001517 try:
1518 sys.exit(main())
1519 except KeyboardInterrupt:
1520 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001521 sys.exit(2)