blob: 7e06021ea8096339ba025e7cfc2530be5fc01694 [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
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800274 def EnsureCQIncludeTrybotsAreAdded(self, cl, bots_to_include, message):
275 """Helper for any PostUploadHook wishing to add CQ_INCLUDE_TRYBOTS.
276
277 Merges the bots_to_include into the current CQ_INCLUDE_TRYBOTS list,
278 keeping it alphabetically sorted. Returns the results that should be
279 returned from the PostUploadHook.
280
281 Args:
282 cl: The git_cl.Changelist object.
283 bots_to_include: A list of strings of bots to include, in the form
284 "master:slave".
285 message: A message to be printed in the case that
286 CQ_INCLUDE_TRYBOTS was updated.
287 """
288 description = cl.GetDescription(force=True)
289 all_bots = []
290 include_re = re.compile(r'^CQ_INCLUDE_TRYBOTS=(.*)', re.M | re.I)
291 m = include_re.search(description)
292 if m:
293 all_bots = [i.strip() for i in m.group(1).split(';') if i.strip()]
294 if set(all_bots) >= set(bots_to_include):
295 return []
296 # Sort the bots to keep them in some consistent order -- not required.
297 all_bots = sorted(set(all_bots) | set(bots_to_include))
298 new_include_trybots = 'CQ_INCLUDE_TRYBOTS=%s' % ';'.join(all_bots)
299 if m:
300 new_description = include_re.sub(new_include_trybots, description)
301 else:
302 new_description = description + '\n' + new_include_trybots + '\n'
303 cl.UpdateDescription(new_description, force=True)
304 return [self.PresubmitNotifyResult(message)]
305
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000306
307class InputApi(object):
308 """An instance of this object is passed to presubmit scripts so they can
309 know stuff about the change they're looking at.
310 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000311 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800312 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000313
maruel@chromium.org3410d912009-06-09 20:56:16 +0000314 # File extensions that are considered source files from a style guide
315 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000316 #
317 # Files without an extension aren't included in the list. If you want to
318 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
319 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000320 DEFAULT_WHITE_LIST = (
321 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000322 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
323 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000324 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000325 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000326 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000327 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000328 )
329
330 # Path regexp that should be excluded from being considered containing source
331 # files. Don't modify this list from a presubmit script!
332 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000333 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000334 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000335 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
336 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000337 # Output directories (just in case)
338 r".*\bDebug[\\\/].*",
339 r".*\bRelease[\\\/].*",
340 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000341 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000342 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000343 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000344 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000345 r"(|.*[\\\/])\.git[\\\/].*",
346 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000347 # There is no point in processing a patch file.
348 r".+\.diff$",
349 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000350 )
351
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000352 def __init__(self, change, presubmit_path, is_committing,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000353 rietveld_obj, verbose, gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000354 """Builds an InputApi object.
355
356 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000357 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000358 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000359 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000360 rietveld_obj: rietveld.Rietveld client object
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000361 gerrit_obj: provides basic Gerrit codereview functionality.
362 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000363 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000364 # Version number of the presubmit_support script.
365 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000366 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000367 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000368 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000369 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000370 self.dry_run = dry_run
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000371 # TBD
372 self.host_url = 'http://codereview.chromium.org'
373 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000374 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000375
376 # We expose various modules and functions as attributes of the input_api
377 # so that presubmit scripts don't have to import them.
378 self.basename = os.path.basename
379 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000380 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000381 self.cStringIO = cStringIO
dcheng091b7db2016-06-16 01:27:51 -0700382 self.fnmatch = fnmatch
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000383 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000384 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000385 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000386 self.os_listdir = os.listdir
387 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000388 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000389 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000390 self.pickle = pickle
391 self.marshal = marshal
392 self.re = re
393 self.subprocess = subprocess
394 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000395 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000396 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000397 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000398 self.urllib2 = urllib2
399
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000400 # To easily fork python.
401 self.python_executable = sys.executable
402 self.environ = os.environ
403
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000404 # InputApi.platform is the platform you're currently running on.
405 self.platform = sys.platform
406
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000407 self.cpu_count = multiprocessing.cpu_count()
408
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000409 # this is done here because in RunTests, the current working directory has
410 # changed, which causes Pool() to explode fantastically when run on windows
411 # (because it tries to load the __main__ module, which imports lots of
412 # things relative to the current working directory).
413 self._run_tests_pool = multiprocessing.Pool(self.cpu_count)
414
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000415 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000416 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000417
418 # We carry the canned checks so presubmit scripts can easily use them.
419 self.canned_checks = presubmit_canned_checks
420
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000421 # TODO(dpranke): figure out a list of all approved owners for a repo
422 # in order to be able to handle wildcard OWNERS files?
423 self.owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -0700424 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000425 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000426 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000427
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000428 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000429 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000430 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800431 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000432 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000433 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000434 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
435 for (a, b, header) in cpplint._re_pattern_templates
436 ]
437
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000438 def PresubmitLocalPath(self):
439 """Returns the local path of the presubmit script currently being run.
440
441 This is useful if you don't want to hard-code absolute paths in the
442 presubmit script. For example, It can be used to find another file
443 relative to the PRESUBMIT.py script, so the whole tree can be branched and
444 the presubmit script still works, without editing its content.
445 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000446 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000447
agable0b65e732016-11-22 09:25:46 -0800448 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000449 """Same as input_api.change.AffectedFiles() except only lists files
450 (and optionally directories) in the same directory as the current presubmit
451 script, or subdirectories thereof.
452 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000453 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000454 if len(dir_with_slash) == 1:
455 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000456
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000457 return filter(
458 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
agable0b65e732016-11-22 09:25:46 -0800459 self.change.AffectedFiles(include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000460
agable0b65e732016-11-22 09:25:46 -0800461 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000462 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800463 paths = [af.LocalPath() for af in self.AffectedFiles()]
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000464 logging.debug("LocalPaths: %s", paths)
465 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000466
agable0b65e732016-11-22 09:25:46 -0800467 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000468 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800469 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000470
agable0b65e732016-11-22 09:25:46 -0800471 def AffectedTestableFiles(self, include_deletes=None):
472 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000473 in the same directory as the current presubmit script, or subdirectories
474 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000475 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000476 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800477 warn("AffectedTestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000478 " is deprecated and ignored" % str(include_deletes),
479 category=DeprecationWarning,
480 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800481 return filter(lambda x: x.IsTestableFile(),
482 self.AffectedFiles(include_deletes=False))
483
484 def AffectedTextFiles(self, include_deletes=None):
485 """An alias to AffectedTestableFiles for backwards compatibility."""
486 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000487
maruel@chromium.org3410d912009-06-09 20:56:16 +0000488 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
489 """Filters out files that aren't considered "source file".
490
491 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
492 and InputApi.DEFAULT_BLACK_LIST is used respectively.
493
494 The lists will be compiled as regular expression and
495 AffectedFile.LocalPath() needs to pass both list.
496
497 Note: Copy-paste this function to suit your needs or use a lambda function.
498 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000499 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000500 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000501 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000502 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000503 return True
504 return False
505 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
506 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
507
508 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800509 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000510
511 If source_file is None, InputApi.FilterSourceFile() is used.
512 """
513 if not source_file:
514 source_file = self.FilterSourceFile
agable0b65e732016-11-22 09:25:46 -0800515 return filter(source_file, self.AffectedTestableFiles())
maruel@chromium.org3410d912009-06-09 20:56:16 +0000516
517 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000518 """An iterator over all text lines in "new" version of changed files.
519
520 Only lists lines from new or modified text files in the change that are
521 contained by the directory of the currently executing presubmit script.
522
523 This is useful for doing line-by-line regex checks, like checking for
524 trailing whitespace.
525
526 Yields:
527 a 3 tuple:
528 the AffectedFile instance of the current file;
529 integer line number (1-based); and
530 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000531
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000532 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000533 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000534 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000535 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000536
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000537 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000538 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000539
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000540 Deny reading anything outside the repository.
541 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000542 if isinstance(file_item, AffectedFile):
543 file_item = file_item.AbsoluteLocalPath()
544 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000545 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000546 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000547
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000548 @property
549 def tbr(self):
550 """Returns if a change is TBR'ed."""
551 return 'TBR' in self.change.tags
552
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000553 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000554 tests = []
555 msgs = []
556 for t in tests_mix:
557 if isinstance(t, OutputApi.PresubmitResult):
558 msgs.append(t)
559 else:
560 assert issubclass(t.message, _PresubmitResult)
561 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000562 if self.verbose:
563 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000564 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000565 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000566 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000567 else:
568 msgs.extend(map(CallCommand, tests))
569 return [m for m in msgs if m]
570
scottmg86099d72016-09-01 09:16:51 -0700571 def ShutdownPool(self):
572 self._run_tests_pool.close()
573 self._run_tests_pool.join()
574 self._run_tests_pool = None
575
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000576
nick@chromium.orgff526192013-06-10 19:30:26 +0000577class _DiffCache(object):
578 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000579 def __init__(self, upstream=None):
580 """Stores the upstream revision against which all diffs will be computed."""
581 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000582
583 def GetDiff(self, path, local_root):
584 """Get the diff for a particular path."""
585 raise NotImplementedError()
586
587
nick@chromium.orgff526192013-06-10 19:30:26 +0000588class _GitDiffCache(_DiffCache):
589 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000590 def __init__(self, upstream):
591 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000592 self._diffs_by_file = None
593
594 def GetDiff(self, path, local_root):
595 if not self._diffs_by_file:
596 # Compute a single diff for all files and parse the output; should
597 # with git this is much faster than computing one diff for each file.
598 diffs = {}
599
600 # Don't specify any filenames below, because there are command line length
601 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000602 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
603 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000604
605 # This regex matches the path twice, separated by a space. Note that
606 # filename itself may contain spaces.
607 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
608 current_diff = []
609 keep_line_endings = True
610 for x in unified_diff.splitlines(keep_line_endings):
611 match = file_marker.match(x)
612 if match:
613 # Marks the start of a new per-file section.
614 diffs[match.group('filename')] = current_diff = [x]
615 elif x.startswith('diff --git'):
616 raise PresubmitFailure('Unexpected diff line: %s' % x)
617 else:
618 current_diff.append(x)
619
620 self._diffs_by_file = dict(
621 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
622
623 if path not in self._diffs_by_file:
624 raise PresubmitFailure(
625 'Unified diff did not contain entry for file %s' % path)
626
627 return self._diffs_by_file[path]
628
629
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000630class AffectedFile(object):
631 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000632
633 DIFF_CACHE = _DiffCache
634
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000635 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800636 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000637 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000638 self._path = path
639 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000640 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000641 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000642 self._cached_changed_contents = None
643 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000644 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700645 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000646
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000647 def LocalPath(self):
648 """Returns the path of this file on the local disk relative to client root.
649 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000650 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000651
652 def AbsoluteLocalPath(self):
653 """Returns the absolute path of this file on the local disk.
654 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000655 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000656
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000657 def Action(self):
658 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000659 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
660 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000661 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000662
agable0b65e732016-11-22 09:25:46 -0800663 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000664 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000665
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000666 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000667 raise NotImplementedError() # Implement when needed
668
agable0b65e732016-11-22 09:25:46 -0800669 def IsTextFile(self):
670 """An alias to IsTestableFile for backwards compatibility."""
671 return self.IsTestableFile()
672
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000673 def NewContents(self):
674 """Returns an iterator over the lines in the new version of file.
675
676 The new version is the file in the user's workspace, i.e. the "right hand
677 side".
678
679 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000680 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000681 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000682 if self._cached_new_contents is None:
683 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800684 try:
685 self._cached_new_contents = gclient_utils.FileRead(
686 self.AbsoluteLocalPath(), 'rU').splitlines()
687 except IOError:
688 pass # File not found? That's fine; maybe it was deleted.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000689 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000690
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000691 def ChangedContents(self):
692 """Returns a list of tuples (line number, line text) of all new lines.
693
694 This relies on the scm diff output describing each changed code section
695 with a line of the form
696
697 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
698 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000699 if self._cached_changed_contents is not None:
700 return self._cached_changed_contents[:]
701 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000702 line_num = 0
703
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000704 for line in self.GenerateScmDiff().splitlines():
705 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
706 if m:
707 line_num = int(m.groups(1)[0])
708 continue
709 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000710 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000711 if not line.startswith('-'):
712 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000713 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000714
maruel@chromium.org5de13972009-06-10 18:16:06 +0000715 def __str__(self):
716 return self.LocalPath()
717
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000718 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000719 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000720
maruel@chromium.org58407af2011-04-12 23:15:57 +0000721
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000722class GitAffectedFile(AffectedFile):
723 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000724 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800725 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000726
nick@chromium.orgff526192013-06-10 19:30:26 +0000727 DIFF_CACHE = _GitDiffCache
728
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000729 def __init__(self, *args, **kwargs):
730 AffectedFile.__init__(self, *args, **kwargs)
731 self._server_path = None
agable0b65e732016-11-22 09:25:46 -0800732 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000733
agable0b65e732016-11-22 09:25:46 -0800734 def IsTestableFile(self):
735 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000736 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -0800737 # A deleted file is not testable.
738 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000739 else:
agable0b65e732016-11-22 09:25:46 -0800740 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
741 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000742
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000743
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000744class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000745 """Describe a change.
746
747 Used directly by the presubmit scripts to query the current change being
748 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000749
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000750 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000751 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000752 self.KEY: equivalent to tags['KEY']
753 """
754
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000755 _AFFECTED_FILES = AffectedFile
756
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000757 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000758 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000759 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000760 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000761
maruel@chromium.org58407af2011-04-12 23:15:57 +0000762 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000763 self, name, description, local_root, files, issue, patchset, author,
764 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000765 if files is None:
766 files = []
767 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000768 # Convert root into an absolute path.
769 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000770 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000771 self.issue = issue
772 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000773 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000774
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000775 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000776 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000777 self._description_without_tags = ''
778 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000779
maruel@chromium.orge085d812011-10-10 19:49:15 +0000780 assert all(
781 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
782
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000783 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000784 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000785 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
786 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000787 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000788
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000789 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000790 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000791 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000792
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000793 def DescriptionText(self):
794 """Returns the user-entered changelist description, minus tags.
795
796 Any line in the user-provided description starting with e.g. "FOO="
797 (whitespace permitted before and around) is considered a tag line. Such
798 lines are stripped out of the description this function returns.
799 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000800 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000801
802 def FullDescriptionText(self):
803 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000804 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000805
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000806 def SetDescriptionText(self, description):
807 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000808
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000809 Also updates the list of tags."""
810 self._full_description = description
811
812 # From the description text, build up a dictionary of key/value pairs
813 # plus the description minus all key/value or "tag" lines.
814 description_without_tags = []
815 self.tags = {}
816 for line in self._full_description.splitlines():
817 m = self.TAG_LINE_RE.match(line)
818 if m:
819 self.tags[m.group('key')] = m.group('value')
820 else:
821 description_without_tags.append(line)
822
823 # Change back to text and remove whitespace at end.
824 self._description_without_tags = (
825 '\n'.join(description_without_tags).rstrip())
826
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000827 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000828 """Returns the repository (checkout) root directory for this change,
829 as an absolute path.
830 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000831 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000832
833 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000834 """Return tags directly as attributes on the object."""
835 if not re.match(r"^[A-Z_]*$", attr):
836 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000837 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000838
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000839 def AllFiles(self, root=None):
840 """List all files under source control in the repo."""
841 raise NotImplementedError()
842
agable0b65e732016-11-22 09:25:46 -0800843 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000844 """Returns a list of AffectedFile instances for all files in the change.
845
846 Args:
847 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +0000848 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000849
850 Returns:
851 [AffectedFile(path, action), AffectedFile(path, action)]
852 """
agable0b65e732016-11-22 09:25:46 -0800853 affected = filter(file_filter, self._affected_files)
sail@chromium.org5538e022011-05-12 17:53:16 +0000854
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000855 if include_deletes:
856 return affected
857 else:
858 return filter(lambda x: x.Action() != 'D', affected)
859
agable0b65e732016-11-22 09:25:46 -0800860 def AffectedTestableFiles(self, include_deletes=None):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000861 """Return a list of the existing text files in a change."""
862 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800863 warn("AffectedTeestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000864 " is deprecated and ignored" % str(include_deletes),
865 category=DeprecationWarning,
866 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800867 return filter(lambda x: x.IsTestableFile(),
868 self.AffectedFiles(include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000869
agable0b65e732016-11-22 09:25:46 -0800870 def AffectedTextFiles(self, include_deletes=None):
871 """An alias to AffectedTestableFiles for backwards compatibility."""
872 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000873
agable0b65e732016-11-22 09:25:46 -0800874 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000875 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800876 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000877
agable0b65e732016-11-22 09:25:46 -0800878 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000879 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800880 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000881
882 def RightHandSideLines(self):
883 """An iterator over all text lines in "new" version of changed files.
884
885 Lists lines from new or modified text files in the change.
886
887 This is useful for doing line-by-line regex checks, like checking for
888 trailing whitespace.
889
890 Yields:
891 a 3 tuple:
892 the AffectedFile instance of the current file;
893 integer line number (1-based); and
894 the contents of the line as a string.
895 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000896 return _RightHandSideLinesImpl(
897 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -0800898 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000899
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000900
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000901class GitChange(Change):
902 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000903 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000904
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000905 def AllFiles(self, root=None):
906 """List all files under source control in the repo."""
907 root = root or self.RepositoryRoot()
908 return subprocess.check_output(
909 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
910
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000911
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000912def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000913 """Finds all presubmit files that apply to a given set of source files.
914
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000915 If inherit-review-settings-ok is present right under root, looks for
916 PRESUBMIT.py in directories enclosing root.
917
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000918 Args:
919 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000920 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000921
922 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000923 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000924 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000925 files = [normpath(os.path.join(root, f)) for f in files]
926
927 # List all the individual directories containing files.
928 directories = set([os.path.dirname(f) for f in files])
929
930 # Ignore root if inherit-review-settings-ok is present.
931 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
932 root = None
933
934 # Collect all unique directories that may contain PRESUBMIT.py.
935 candidates = set()
936 for directory in directories:
937 while True:
938 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000939 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000940 candidates.add(directory)
941 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000942 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000943 parent_dir = os.path.dirname(directory)
944 if parent_dir == directory:
945 # We hit the system root directory.
946 break
947 directory = parent_dir
948
949 # Look for PRESUBMIT.py in all candidate directories.
950 results = []
951 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -0700952 try:
953 for f in os.listdir(directory):
954 p = os.path.join(directory, f)
955 if os.path.isfile(p) and re.match(
956 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
957 results.append(p)
958 except OSError:
959 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000960
tobiasjs2836bcf2016-08-16 04:08:16 -0700961 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000962 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000963
964
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +0000965class GetTryMastersExecuter(object):
966 @staticmethod
967 def ExecPresubmitScript(script_text, presubmit_path, project, change):
968 """Executes GetPreferredTryMasters() from a single presubmit script.
969
970 Args:
971 script_text: The text of the presubmit script.
972 presubmit_path: Project script to run.
973 project: Project name to pass to presubmit script for bot selection.
974
975 Return:
976 A map of try masters to map of builders to set of tests.
977 """
978 context = {}
979 try:
980 exec script_text in context
981 except Exception, e:
982 raise PresubmitFailure('"%s" had an exception.\n%s'
983 % (presubmit_path, e))
984
985 function_name = 'GetPreferredTryMasters'
986 if function_name not in context:
987 return {}
988 get_preferred_try_masters = context[function_name]
989 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
990 raise PresubmitFailure(
991 'Expected function "GetPreferredTryMasters" to take two arguments.')
992 return get_preferred_try_masters(project, change)
993
994
rmistry@google.com5626a922015-02-26 14:03:30 +0000995class GetPostUploadExecuter(object):
996 @staticmethod
997 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
998 """Executes PostUploadHook() from a single presubmit script.
999
1000 Args:
1001 script_text: The text of the presubmit script.
1002 presubmit_path: Project script to run.
1003 cl: The Changelist object.
1004 change: The Change object.
1005
1006 Return:
1007 A list of results objects.
1008 """
1009 context = {}
1010 try:
1011 exec script_text in context
1012 except Exception, e:
1013 raise PresubmitFailure('"%s" had an exception.\n%s'
1014 % (presubmit_path, e))
1015
1016 function_name = 'PostUploadHook'
1017 if function_name not in context:
1018 return {}
1019 post_upload_hook = context[function_name]
1020 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1021 raise PresubmitFailure(
1022 'Expected function "PostUploadHook" to take three arguments.')
1023 return post_upload_hook(cl, change, OutputApi(False))
1024
1025
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001026def _MergeMasters(masters1, masters2):
1027 """Merges two master maps. Merges also the tests of each builder."""
1028 result = {}
1029 for (master, builders) in itertools.chain(masters1.iteritems(),
1030 masters2.iteritems()):
1031 new_builders = result.setdefault(master, {})
1032 for (builder, tests) in builders.iteritems():
1033 new_builders.setdefault(builder, set([])).update(tests)
1034 return result
1035
1036
1037def DoGetTryMasters(change,
1038 changed_files,
1039 repository_root,
1040 default_presubmit,
1041 project,
1042 verbose,
1043 output_stream):
1044 """Get the list of try masters from the presubmit scripts.
1045
1046 Args:
1047 changed_files: List of modified files.
1048 repository_root: The repository root.
1049 default_presubmit: A default presubmit script to execute in any case.
1050 project: Optional name of a project used in selecting trybots.
1051 verbose: Prints debug info.
1052 output_stream: A stream to write debug output to.
1053
1054 Return:
1055 Map of try masters to map of builders to set of tests.
1056 """
1057 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1058 if not presubmit_files and verbose:
1059 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1060 results = {}
1061 executer = GetTryMastersExecuter()
1062
1063 if default_presubmit:
1064 if verbose:
1065 output_stream.write("Running default presubmit script.\n")
1066 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1067 results = _MergeMasters(results, executer.ExecPresubmitScript(
1068 default_presubmit, fake_path, project, change))
1069 for filename in presubmit_files:
1070 filename = os.path.abspath(filename)
1071 if verbose:
1072 output_stream.write("Running %s\n" % filename)
1073 # Accept CRLF presubmit script.
1074 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1075 results = _MergeMasters(results, executer.ExecPresubmitScript(
1076 presubmit_script, filename, project, change))
1077
1078 # Make sets to lists again for later JSON serialization.
1079 for builders in results.itervalues():
1080 for builder in builders:
1081 builders[builder] = list(builders[builder])
1082
1083 if results and verbose:
1084 output_stream.write('%s\n' % str(results))
1085 return results
1086
1087
rmistry@google.com5626a922015-02-26 14:03:30 +00001088def DoPostUploadExecuter(change,
1089 cl,
1090 repository_root,
1091 verbose,
1092 output_stream):
1093 """Execute the post upload hook.
1094
1095 Args:
1096 change: The Change object.
1097 cl: The Changelist object.
1098 repository_root: The repository root.
1099 verbose: Prints debug info.
1100 output_stream: A stream to write debug output to.
1101 """
1102 presubmit_files = ListRelevantPresubmitFiles(
1103 change.LocalPaths(), repository_root)
1104 if not presubmit_files and verbose:
1105 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1106 results = []
1107 executer = GetPostUploadExecuter()
1108 # The root presubmit file should be executed after the ones in subdirectories.
1109 # i.e. the specific post upload hooks should run before the general ones.
1110 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1111 presubmit_files.reverse()
1112
1113 for filename in presubmit_files:
1114 filename = os.path.abspath(filename)
1115 if verbose:
1116 output_stream.write("Running %s\n" % filename)
1117 # Accept CRLF presubmit script.
1118 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1119 results.extend(executer.ExecPresubmitScript(
1120 presubmit_script, filename, cl, change))
1121 output_stream.write('\n')
1122 if results:
1123 output_stream.write('** Post Upload Hook Messages **\n')
1124 for result in results:
1125 result.handle(output_stream)
1126 output_stream.write('\n')
1127
1128 return results
1129
1130
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001131class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001132 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001133 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001134 """
1135 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001136 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001137 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001138 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001139 gerrit_obj: provides basic Gerrit codereview functionality.
1140 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001141 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001142 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001143 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001144 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001145 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001146 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001147 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001148
1149 def ExecPresubmitScript(self, script_text, presubmit_path):
1150 """Executes a single presubmit script.
1151
1152 Args:
1153 script_text: The text of the presubmit script.
1154 presubmit_path: The path to the presubmit file (this will be reported via
1155 input_api.PresubmitLocalPath()).
1156
1157 Return:
1158 A list of result objects, empty if no problems.
1159 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001160
chase@chromium.org8e416c82009-10-06 04:30:44 +00001161 # Change to the presubmit file's directory to support local imports.
1162 main_path = os.getcwd()
1163 os.chdir(os.path.dirname(presubmit_path))
1164
1165 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001166 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001167 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001168 gerrit_obj=self.gerrit, dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001169 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001170 try:
1171 exec script_text in context
1172 except Exception, e:
1173 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001174
1175 # These function names must change if we make substantial changes to
1176 # the presubmit API that are not backwards compatible.
1177 if self.committing:
1178 function_name = 'CheckChangeOnCommit'
1179 else:
1180 function_name = 'CheckChangeOnUpload'
1181 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001182 context['__args'] = (input_api, OutputApi(self.committing))
tobiasjs2836bcf2016-08-16 04:08:16 -07001183 logging.debug('Running %s in %s', function_name, presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001184 result = eval(function_name + '(*__args)', context)
tobiasjs2836bcf2016-08-16 04:08:16 -07001185 logging.debug('Running %s done.', function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001186 if not (isinstance(result, types.TupleType) or
1187 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001188 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001189 'Presubmit functions must return a tuple or list')
1190 for item in result:
1191 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001192 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001193 'All presubmit results must be of types derived from '
1194 'output_api.PresubmitResult')
1195 else:
1196 result = () # no error since the script doesn't care about current event.
1197
scottmg86099d72016-09-01 09:16:51 -07001198 input_api.ShutdownPool()
1199
chase@chromium.org8e416c82009-10-06 04:30:44 +00001200 # Return the process to the original working directory.
1201 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001202 return result
1203
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001204def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001205 committing,
1206 verbose,
1207 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001208 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001209 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001210 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001211 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001212 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001213 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001214 """Runs all presubmit checks that apply to the files in the change.
1215
1216 This finds all PRESUBMIT.py files in directories enclosing the files in the
1217 change (up to the repository root) and calls the relevant entrypoint function
1218 depending on whether the change is being committed or uploaded.
1219
1220 Prints errors, warnings and notifications. Prompts the user for warnings
1221 when needed.
1222
1223 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001224 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001225 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001226 verbose: Prints debug info.
1227 output_stream: A stream to write output from presubmit tests to.
1228 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001229 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001230 may_prompt: Enable (y/n) questions on warning or error. If False,
1231 any questions are answered with yes by default.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001232 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001233 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001234 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001235
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001236 Warning:
1237 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1238 SHOULD be sys.stdin.
1239
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001240 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001241 A PresubmitOutput object. Use output.should_continue() to figure out
1242 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001243 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001244 old_environ = os.environ
1245 try:
1246 # Make sure python subprocesses won't generate .pyc files.
1247 os.environ = os.environ.copy()
1248 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001249
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001250 output = PresubmitOutput(input_stream, output_stream)
1251 if committing:
1252 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001253 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001254 output.write("Running presubmit upload checks ...\n")
1255 start_time = time.time()
1256 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001257 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001258 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001259 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001260 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001261 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001262 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001263 if default_presubmit:
1264 if verbose:
1265 output.write("Running default presubmit script.\n")
1266 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1267 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1268 for filename in presubmit_files:
1269 filename = os.path.abspath(filename)
1270 if verbose:
1271 output.write("Running %s\n" % filename)
1272 # Accept CRLF presubmit script.
1273 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1274 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001275
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001276 errors = []
1277 notifications = []
1278 warnings = []
1279 for result in results:
1280 if result.fatal:
1281 errors.append(result)
1282 elif result.should_prompt:
1283 warnings.append(result)
1284 else:
1285 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001286
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001287 output.write('\n')
1288 for name, items in (('Messages', notifications),
1289 ('Warnings', warnings),
1290 ('ERRORS', errors)):
1291 if items:
1292 output.write('** Presubmit %s **\n' % name)
1293 for item in items:
1294 item.handle(output)
1295 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001296
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001297 total_time = time.time() - start_time
1298 if total_time > 1.0:
1299 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001300
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001301 if errors:
1302 output.fail()
1303 elif warnings:
1304 output.write('There were presubmit warnings. ')
1305 if may_prompt:
1306 output.prompt_yes_no('Are you sure you wish to continue? (y/N): ')
1307 else:
1308 output.write('Presubmit checks passed.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001309
1310 global _ASKED_FOR_FEEDBACK
1311 # Ask for feedback one time out of 5.
1312 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001313 output.write(
1314 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1315 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1316 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001317 _ASKED_FOR_FEEDBACK = True
1318 return output
1319 finally:
1320 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001321
1322
1323def ScanSubDirs(mask, recursive):
1324 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001325 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001326 else:
1327 results = []
1328 for root, dirs, files in os.walk('.'):
1329 if '.svn' in dirs:
1330 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001331 if '.git' in dirs:
1332 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001333 for name in files:
1334 if fnmatch.fnmatch(name, mask):
1335 results.append(os.path.join(root, name))
1336 return results
1337
1338
1339def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001340 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001341 files = []
1342 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001343 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001344 return files
1345
1346
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001347def load_files(options, args):
1348 """Tries to determine the SCM."""
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001349 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001350 if args:
1351 files = ParseFiles(args, options.recursive)
agable0b65e732016-11-22 09:25:46 -08001352 change_scm = scm.determine_scm(options.root)
1353 if change_scm == 'git':
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001354 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001355 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001356 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001357 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001358 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001359 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001360 if not files:
1361 return None, None
1362 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001363 return change_class, files
1364
1365
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001366class NonexistantCannedCheckFilter(Exception):
1367 pass
1368
1369
1370@contextlib.contextmanager
1371def canned_check_filter(method_names):
1372 filtered = {}
1373 try:
1374 for method_name in method_names:
1375 if not hasattr(presubmit_canned_checks, method_name):
1376 raise NonexistantCannedCheckFilter(method_name)
1377 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1378 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1379 yield
1380 finally:
1381 for name, method in filtered.iteritems():
1382 setattr(presubmit_canned_checks, name, method)
1383
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001384
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001385def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001386 """Runs an external program, potentially from a child process created by the
1387 multiprocessing module.
1388
1389 multiprocessing needs a top level function with a single argument.
1390 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001391 cmd_data.kwargs['stdout'] = subprocess.PIPE
1392 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1393 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001394 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001395 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001396 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001397 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001398 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001399 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001400 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1401 if code != 0:
1402 return cmd_data.message(
1403 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1404 if cmd_data.info:
1405 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001406
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001407
sbc@chromium.org013731e2015-02-26 18:28:43 +00001408def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001409 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001410 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001411 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001412 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001413 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1414 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001415 parser.add_option("-r", "--recursive", action="store_true",
1416 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001417 parser.add_option("-v", "--verbose", action="count", default=0,
1418 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001419 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001420 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001421 parser.add_option("--description", default='')
1422 parser.add_option("--issue", type='int', default=0)
1423 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001424 parser.add_option("--root", default=os.getcwd(),
1425 help="Search for PRESUBMIT.py up to this directory. "
1426 "If inherit-review-settings-ok is present in this "
1427 "directory, parent directories up to the root file "
1428 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001429 parser.add_option("--upstream",
1430 help="Git only: the base ref or upstream branch against "
1431 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001432 parser.add_option("--default_presubmit")
1433 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001434 parser.add_option("--skip_canned", action='append', default=[],
1435 help="A list of checks to skip which appear in "
1436 "presubmit_canned_checks. Can be provided multiple times "
1437 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001438 parser.add_option("--dry_run", action='store_true',
1439 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001440 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001441 parser.add_option("--gerrit_fetch", action='store_true',
1442 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001443 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1444 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001445 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1446 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001447 # These are for OAuth2 authentication for bots. See also apply_issue.py
1448 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1449 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1450
phajdan.jr@chromium.org2ff30182016-03-23 09:52:51 +00001451 # TODO(phajdan.jr): Update callers and remove obsolete --trybot-json .
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001452 parser.add_option("--trybot-json",
1453 help="Output trybot information to the file specified.")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001454 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001455 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001456 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001457
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001458 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001459 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001460 elif options.verbose:
1461 logging.basicConfig(level=logging.INFO)
1462 else:
1463 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001464
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001465 if (any((options.rietveld_url, options.rietveld_email_file,
1466 options.rietveld_fetch, options.rietveld_private_key_file))
1467 and any((options.gerrit_url, options.gerrit_fetch))):
1468 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1469 'allowed')
1470
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001471 if options.rietveld_email and options.rietveld_email_file:
1472 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1473 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001474 if options.rietveld_email_file:
1475 with open(options.rietveld_email_file, "rb") as f:
1476 options.rietveld_email = f.read().strip()
1477
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001478 change_class, files = load_files(options, args)
1479 if not change_class:
1480 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001481 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001482
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001483 rietveld_obj, gerrit_obj = None, None
1484
maruel@chromium.org239f4112011-06-03 20:08:23 +00001485 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001486 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001487 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001488 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1489 options.rietveld_url,
1490 options.rietveld_email,
1491 options.rietveld_private_key_file)
1492 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001493 rietveld_obj = rietveld.CachingRietveld(
1494 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001495 auth_config,
1496 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001497 if options.rietveld_fetch:
1498 assert options.issue
1499 props = rietveld_obj.get_issue_properties(options.issue, False)
1500 options.author = props['owner_email']
1501 options.description = props['description']
1502 logging.info('Got author: "%s"', options.author)
1503 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001504
1505 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001506 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001507 rietveld_obj = None
1508 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1509 options.author = gerrit_obj.GetChangeOwner(options.issue)
1510 options.description = gerrit_obj.GetChangeDescription(options.issue,
1511 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001512 logging.info('Got author: "%s"', options.author)
1513 logging.info('Got description: """\n%s\n"""', options.description)
1514
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001515 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001516 with canned_check_filter(options.skip_canned):
1517 results = DoPresubmitChecks(
1518 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001519 options.description,
1520 options.root,
1521 files,
1522 options.issue,
1523 options.patchset,
1524 options.author,
1525 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001526 options.commit,
1527 options.verbose,
1528 sys.stdout,
1529 sys.stdin,
1530 options.default_presubmit,
1531 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001532 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001533 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001534 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001535 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001536 except NonexistantCannedCheckFilter, e:
1537 print >> sys.stderr, (
1538 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1539 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001540 except PresubmitFailure, e:
1541 print >> sys.stderr, e
1542 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1543 print >> sys.stderr, 'If all fails, contact maruel@'
1544 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001545
1546
1547if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001548 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001549 try:
1550 sys.exit(main())
1551 except KeyboardInterrupt:
1552 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001553 sys.exit(2)