blob: b35d82645d89bce925a61831bec51ff51193d599 [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
Jochen Eisinger72606f82017-04-04 10:44:18 +0200421
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000422 # TODO(dpranke): figure out a list of all approved owners for a repo
423 # in order to be able to handle wildcard OWNERS files?
424 self.owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +0200425 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000426 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000427 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000428
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000429 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000430 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000431 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800432 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000433 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000434 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000435 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
436 for (a, b, header) in cpplint._re_pattern_templates
437 ]
438
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000439 def PresubmitLocalPath(self):
440 """Returns the local path of the presubmit script currently being run.
441
442 This is useful if you don't want to hard-code absolute paths in the
443 presubmit script. For example, It can be used to find another file
444 relative to the PRESUBMIT.py script, so the whole tree can be branched and
445 the presubmit script still works, without editing its content.
446 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000447 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000448
agable0b65e732016-11-22 09:25:46 -0800449 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000450 """Same as input_api.change.AffectedFiles() except only lists files
451 (and optionally directories) in the same directory as the current presubmit
452 script, or subdirectories thereof.
453 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000454 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000455 if len(dir_with_slash) == 1:
456 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000457
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000458 return filter(
459 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
agable0b65e732016-11-22 09:25:46 -0800460 self.change.AffectedFiles(include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000461
agable0b65e732016-11-22 09:25:46 -0800462 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000463 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800464 paths = [af.LocalPath() for af in self.AffectedFiles()]
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000465 logging.debug("LocalPaths: %s", paths)
466 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000467
agable0b65e732016-11-22 09:25:46 -0800468 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000469 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800470 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000471
agable0b65e732016-11-22 09:25:46 -0800472 def AffectedTestableFiles(self, include_deletes=None):
473 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000474 in the same directory as the current presubmit script, or subdirectories
475 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000476 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000477 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800478 warn("AffectedTestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000479 " is deprecated and ignored" % str(include_deletes),
480 category=DeprecationWarning,
481 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800482 return filter(lambda x: x.IsTestableFile(),
483 self.AffectedFiles(include_deletes=False))
484
485 def AffectedTextFiles(self, include_deletes=None):
486 """An alias to AffectedTestableFiles for backwards compatibility."""
487 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000488
maruel@chromium.org3410d912009-06-09 20:56:16 +0000489 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
490 """Filters out files that aren't considered "source file".
491
492 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
493 and InputApi.DEFAULT_BLACK_LIST is used respectively.
494
495 The lists will be compiled as regular expression and
496 AffectedFile.LocalPath() needs to pass both list.
497
498 Note: Copy-paste this function to suit your needs or use a lambda function.
499 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000500 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000501 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000502 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000503 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000504 return True
505 return False
506 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
507 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
508
509 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800510 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000511
512 If source_file is None, InputApi.FilterSourceFile() is used.
513 """
514 if not source_file:
515 source_file = self.FilterSourceFile
agable0b65e732016-11-22 09:25:46 -0800516 return filter(source_file, self.AffectedTestableFiles())
maruel@chromium.org3410d912009-06-09 20:56:16 +0000517
518 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000519 """An iterator over all text lines in "new" version of changed files.
520
521 Only lists lines from new or modified text files in the change that are
522 contained by the directory of the currently executing presubmit script.
523
524 This is useful for doing line-by-line regex checks, like checking for
525 trailing whitespace.
526
527 Yields:
528 a 3 tuple:
529 the AffectedFile instance of the current file;
530 integer line number (1-based); and
531 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000532
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000533 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000534 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000535 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000536 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000537
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000538 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000539 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000540
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000541 Deny reading anything outside the repository.
542 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000543 if isinstance(file_item, AffectedFile):
544 file_item = file_item.AbsoluteLocalPath()
545 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000546 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000547 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000548
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000549 @property
550 def tbr(self):
551 """Returns if a change is TBR'ed."""
552 return 'TBR' in self.change.tags
553
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000554 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000555 tests = []
556 msgs = []
557 for t in tests_mix:
558 if isinstance(t, OutputApi.PresubmitResult):
559 msgs.append(t)
560 else:
561 assert issubclass(t.message, _PresubmitResult)
562 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000563 if self.verbose:
564 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000565 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000566 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000567 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000568 else:
569 msgs.extend(map(CallCommand, tests))
570 return [m for m in msgs if m]
571
scottmg86099d72016-09-01 09:16:51 -0700572 def ShutdownPool(self):
573 self._run_tests_pool.close()
574 self._run_tests_pool.join()
575 self._run_tests_pool = None
576
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000577
nick@chromium.orgff526192013-06-10 19:30:26 +0000578class _DiffCache(object):
579 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000580 def __init__(self, upstream=None):
581 """Stores the upstream revision against which all diffs will be computed."""
582 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000583
584 def GetDiff(self, path, local_root):
585 """Get the diff for a particular path."""
586 raise NotImplementedError()
587
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700588 def GetOldContents(self, path, local_root):
589 """Get the old version for a particular path."""
590 raise NotImplementedError()
591
nick@chromium.orgff526192013-06-10 19:30:26 +0000592
nick@chromium.orgff526192013-06-10 19:30:26 +0000593class _GitDiffCache(_DiffCache):
594 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000595 def __init__(self, upstream):
596 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000597 self._diffs_by_file = None
598
599 def GetDiff(self, path, local_root):
600 if not self._diffs_by_file:
601 # Compute a single diff for all files and parse the output; should
602 # with git this is much faster than computing one diff for each file.
603 diffs = {}
604
605 # Don't specify any filenames below, because there are command line length
606 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000607 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
608 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000609
610 # This regex matches the path twice, separated by a space. Note that
611 # filename itself may contain spaces.
612 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
613 current_diff = []
614 keep_line_endings = True
615 for x in unified_diff.splitlines(keep_line_endings):
616 match = file_marker.match(x)
617 if match:
618 # Marks the start of a new per-file section.
619 diffs[match.group('filename')] = current_diff = [x]
620 elif x.startswith('diff --git'):
621 raise PresubmitFailure('Unexpected diff line: %s' % x)
622 else:
623 current_diff.append(x)
624
625 self._diffs_by_file = dict(
626 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
627
628 if path not in self._diffs_by_file:
629 raise PresubmitFailure(
630 'Unified diff did not contain entry for file %s' % path)
631
632 return self._diffs_by_file[path]
633
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700634 def GetOldContents(self, path, local_root):
635 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
636
nick@chromium.orgff526192013-06-10 19:30:26 +0000637
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000638class AffectedFile(object):
639 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000640
641 DIFF_CACHE = _DiffCache
642
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000643 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800644 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000645 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000646 self._path = path
647 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000648 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000649 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000650 self._cached_changed_contents = None
651 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000652 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700653 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000654
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000655 def LocalPath(self):
656 """Returns the path of this file on the local disk relative to client root.
657 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000658 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000659
660 def AbsoluteLocalPath(self):
661 """Returns the absolute path of this file on the local disk.
662 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000663 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000664
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000665 def Action(self):
666 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000667 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000668
agable0b65e732016-11-22 09:25:46 -0800669 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000670 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000671
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000672 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000673 raise NotImplementedError() # Implement when needed
674
agable0b65e732016-11-22 09:25:46 -0800675 def IsTextFile(self):
676 """An alias to IsTestableFile for backwards compatibility."""
677 return self.IsTestableFile()
678
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700679 def OldContents(self):
680 """Returns an iterator over the lines in the old version of file.
681
Daniel Cheng2da34fe2017-03-21 20:42:12 -0700682 The old version is the file before any modifications in the user's
683 workspace, i.e. the "left hand side".
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700684
685 Contents will be empty if the file is a directory or does not exist.
686 Note: The carriage returns (LF or CR) are stripped off.
687 """
688 return self._diff_cache.GetOldContents(self.LocalPath(),
689 self._local_root).splitlines()
690
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000691 def NewContents(self):
692 """Returns an iterator over the lines in the new version of file.
693
694 The new version is the file in the user's workspace, i.e. the "right hand
695 side".
696
697 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000698 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000699 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000700 if self._cached_new_contents is None:
701 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800702 try:
703 self._cached_new_contents = gclient_utils.FileRead(
704 self.AbsoluteLocalPath(), 'rU').splitlines()
705 except IOError:
706 pass # File not found? That's fine; maybe it was deleted.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000707 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000708
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000709 def ChangedContents(self):
710 """Returns a list of tuples (line number, line text) of all new lines.
711
712 This relies on the scm diff output describing each changed code section
713 with a line of the form
714
715 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
716 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000717 if self._cached_changed_contents is not None:
718 return self._cached_changed_contents[:]
719 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000720 line_num = 0
721
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000722 for line in self.GenerateScmDiff().splitlines():
723 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
724 if m:
725 line_num = int(m.groups(1)[0])
726 continue
727 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000728 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000729 if not line.startswith('-'):
730 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000731 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000732
maruel@chromium.org5de13972009-06-10 18:16:06 +0000733 def __str__(self):
734 return self.LocalPath()
735
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000736 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000737 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000738
maruel@chromium.org58407af2011-04-12 23:15:57 +0000739
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000740class GitAffectedFile(AffectedFile):
741 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000742 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800743 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000744
nick@chromium.orgff526192013-06-10 19:30:26 +0000745 DIFF_CACHE = _GitDiffCache
746
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000747 def __init__(self, *args, **kwargs):
748 AffectedFile.__init__(self, *args, **kwargs)
749 self._server_path = None
agable0b65e732016-11-22 09:25:46 -0800750 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000751
agable0b65e732016-11-22 09:25:46 -0800752 def IsTestableFile(self):
753 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000754 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -0800755 # A deleted file is not testable.
756 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000757 else:
agable0b65e732016-11-22 09:25:46 -0800758 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
759 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000760
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000761
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000762class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000763 """Describe a change.
764
765 Used directly by the presubmit scripts to query the current change being
766 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000767
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000768 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000769 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000770 self.KEY: equivalent to tags['KEY']
771 """
772
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000773 _AFFECTED_FILES = AffectedFile
774
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000775 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000776 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000777 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000778 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000779
maruel@chromium.org58407af2011-04-12 23:15:57 +0000780 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000781 self, name, description, local_root, files, issue, patchset, author,
782 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000783 if files is None:
784 files = []
785 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000786 # Convert root into an absolute path.
787 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000788 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000789 self.issue = issue
790 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000791 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000792
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000793 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000794 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000795 self._description_without_tags = ''
796 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000797
maruel@chromium.orge085d812011-10-10 19:49:15 +0000798 assert all(
799 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
800
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000801 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000802 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000803 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
804 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000805 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000806
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000807 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000808 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000809 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000810
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000811 def DescriptionText(self):
812 """Returns the user-entered changelist description, minus tags.
813
814 Any line in the user-provided description starting with e.g. "FOO="
815 (whitespace permitted before and around) is considered a tag line. Such
816 lines are stripped out of the description this function returns.
817 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000818 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000819
820 def FullDescriptionText(self):
821 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000822 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000823
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000824 def SetDescriptionText(self, description):
825 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000826
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000827 Also updates the list of tags."""
828 self._full_description = description
829
830 # From the description text, build up a dictionary of key/value pairs
831 # plus the description minus all key/value or "tag" lines.
832 description_without_tags = []
833 self.tags = {}
834 for line in self._full_description.splitlines():
835 m = self.TAG_LINE_RE.match(line)
836 if m:
837 self.tags[m.group('key')] = m.group('value')
838 else:
839 description_without_tags.append(line)
840
841 # Change back to text and remove whitespace at end.
842 self._description_without_tags = (
843 '\n'.join(description_without_tags).rstrip())
844
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000845 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000846 """Returns the repository (checkout) root directory for this change,
847 as an absolute path.
848 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000849 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000850
851 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000852 """Return tags directly as attributes on the object."""
853 if not re.match(r"^[A-Z_]*$", attr):
854 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000855 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000856
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000857 def AllFiles(self, root=None):
858 """List all files under source control in the repo."""
859 raise NotImplementedError()
860
agable0b65e732016-11-22 09:25:46 -0800861 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000862 """Returns a list of AffectedFile instances for all files in the change.
863
864 Args:
865 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +0000866 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000867
868 Returns:
869 [AffectedFile(path, action), AffectedFile(path, action)]
870 """
agable0b65e732016-11-22 09:25:46 -0800871 affected = filter(file_filter, self._affected_files)
sail@chromium.org5538e022011-05-12 17:53:16 +0000872
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000873 if include_deletes:
874 return affected
Lei Zhang9611c4c2017-04-04 01:41:56 -0700875 return filter(lambda x: x.Action() != 'D', affected)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000876
agable0b65e732016-11-22 09:25:46 -0800877 def AffectedTestableFiles(self, include_deletes=None):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000878 """Return a list of the existing text files in a change."""
879 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800880 warn("AffectedTeestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000881 " is deprecated and ignored" % str(include_deletes),
882 category=DeprecationWarning,
883 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800884 return filter(lambda x: x.IsTestableFile(),
885 self.AffectedFiles(include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000886
agable0b65e732016-11-22 09:25:46 -0800887 def AffectedTextFiles(self, include_deletes=None):
888 """An alias to AffectedTestableFiles for backwards compatibility."""
889 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000890
agable0b65e732016-11-22 09:25:46 -0800891 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000892 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800893 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000894
agable0b65e732016-11-22 09:25:46 -0800895 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000896 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800897 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000898
899 def RightHandSideLines(self):
900 """An iterator over all text lines in "new" version of changed files.
901
902 Lists lines from new or modified text files in the change.
903
904 This is useful for doing line-by-line regex checks, like checking for
905 trailing whitespace.
906
907 Yields:
908 a 3 tuple:
909 the AffectedFile instance of the current file;
910 integer line number (1-based); and
911 the contents of the line as a string.
912 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000913 return _RightHandSideLinesImpl(
914 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -0800915 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000916
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000917
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000918class GitChange(Change):
919 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000920 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000921
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000922 def AllFiles(self, root=None):
923 """List all files under source control in the repo."""
924 root = root or self.RepositoryRoot()
925 return subprocess.check_output(
926 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
927
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000928
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000929def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000930 """Finds all presubmit files that apply to a given set of source files.
931
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000932 If inherit-review-settings-ok is present right under root, looks for
933 PRESUBMIT.py in directories enclosing root.
934
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000935 Args:
936 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000937 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000938
939 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000940 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000941 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000942 files = [normpath(os.path.join(root, f)) for f in files]
943
944 # List all the individual directories containing files.
945 directories = set([os.path.dirname(f) for f in files])
946
947 # Ignore root if inherit-review-settings-ok is present.
948 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
949 root = None
950
951 # Collect all unique directories that may contain PRESUBMIT.py.
952 candidates = set()
953 for directory in directories:
954 while True:
955 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000956 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000957 candidates.add(directory)
958 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000959 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000960 parent_dir = os.path.dirname(directory)
961 if parent_dir == directory:
962 # We hit the system root directory.
963 break
964 directory = parent_dir
965
966 # Look for PRESUBMIT.py in all candidate directories.
967 results = []
968 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -0700969 try:
970 for f in os.listdir(directory):
971 p = os.path.join(directory, f)
972 if os.path.isfile(p) and re.match(
973 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
974 results.append(p)
975 except OSError:
976 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000977
tobiasjs2836bcf2016-08-16 04:08:16 -0700978 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000979 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000980
981
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +0000982class GetTryMastersExecuter(object):
983 @staticmethod
984 def ExecPresubmitScript(script_text, presubmit_path, project, change):
985 """Executes GetPreferredTryMasters() from a single presubmit script.
986
987 Args:
988 script_text: The text of the presubmit script.
989 presubmit_path: Project script to run.
990 project: Project name to pass to presubmit script for bot selection.
991
992 Return:
993 A map of try masters to map of builders to set of tests.
994 """
995 context = {}
996 try:
997 exec script_text in context
998 except Exception, e:
999 raise PresubmitFailure('"%s" had an exception.\n%s'
1000 % (presubmit_path, e))
1001
1002 function_name = 'GetPreferredTryMasters'
1003 if function_name not in context:
1004 return {}
1005 get_preferred_try_masters = context[function_name]
1006 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1007 raise PresubmitFailure(
1008 'Expected function "GetPreferredTryMasters" to take two arguments.')
1009 return get_preferred_try_masters(project, change)
1010
1011
rmistry@google.com5626a922015-02-26 14:03:30 +00001012class GetPostUploadExecuter(object):
1013 @staticmethod
1014 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1015 """Executes PostUploadHook() from a single presubmit script.
1016
1017 Args:
1018 script_text: The text of the presubmit script.
1019 presubmit_path: Project script to run.
1020 cl: The Changelist object.
1021 change: The Change object.
1022
1023 Return:
1024 A list of results objects.
1025 """
1026 context = {}
1027 try:
1028 exec script_text in context
1029 except Exception, e:
1030 raise PresubmitFailure('"%s" had an exception.\n%s'
1031 % (presubmit_path, e))
1032
1033 function_name = 'PostUploadHook'
1034 if function_name not in context:
1035 return {}
1036 post_upload_hook = context[function_name]
1037 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1038 raise PresubmitFailure(
1039 'Expected function "PostUploadHook" to take three arguments.')
1040 return post_upload_hook(cl, change, OutputApi(False))
1041
1042
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001043def _MergeMasters(masters1, masters2):
1044 """Merges two master maps. Merges also the tests of each builder."""
1045 result = {}
1046 for (master, builders) in itertools.chain(masters1.iteritems(),
1047 masters2.iteritems()):
1048 new_builders = result.setdefault(master, {})
1049 for (builder, tests) in builders.iteritems():
1050 new_builders.setdefault(builder, set([])).update(tests)
1051 return result
1052
1053
1054def DoGetTryMasters(change,
1055 changed_files,
1056 repository_root,
1057 default_presubmit,
1058 project,
1059 verbose,
1060 output_stream):
1061 """Get the list of try masters from the presubmit scripts.
1062
1063 Args:
1064 changed_files: List of modified files.
1065 repository_root: The repository root.
1066 default_presubmit: A default presubmit script to execute in any case.
1067 project: Optional name of a project used in selecting trybots.
1068 verbose: Prints debug info.
1069 output_stream: A stream to write debug output to.
1070
1071 Return:
1072 Map of try masters to map of builders to set of tests.
1073 """
1074 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1075 if not presubmit_files and verbose:
1076 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1077 results = {}
1078 executer = GetTryMastersExecuter()
1079
1080 if default_presubmit:
1081 if verbose:
1082 output_stream.write("Running default presubmit script.\n")
1083 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1084 results = _MergeMasters(results, executer.ExecPresubmitScript(
1085 default_presubmit, fake_path, project, change))
1086 for filename in presubmit_files:
1087 filename = os.path.abspath(filename)
1088 if verbose:
1089 output_stream.write("Running %s\n" % filename)
1090 # Accept CRLF presubmit script.
1091 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1092 results = _MergeMasters(results, executer.ExecPresubmitScript(
1093 presubmit_script, filename, project, change))
1094
1095 # Make sets to lists again for later JSON serialization.
1096 for builders in results.itervalues():
1097 for builder in builders:
1098 builders[builder] = list(builders[builder])
1099
1100 if results and verbose:
1101 output_stream.write('%s\n' % str(results))
1102 return results
1103
1104
rmistry@google.com5626a922015-02-26 14:03:30 +00001105def DoPostUploadExecuter(change,
1106 cl,
1107 repository_root,
1108 verbose,
1109 output_stream):
1110 """Execute the post upload hook.
1111
1112 Args:
1113 change: The Change object.
1114 cl: The Changelist object.
1115 repository_root: The repository root.
1116 verbose: Prints debug info.
1117 output_stream: A stream to write debug output to.
1118 """
1119 presubmit_files = ListRelevantPresubmitFiles(
1120 change.LocalPaths(), repository_root)
1121 if not presubmit_files and verbose:
1122 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1123 results = []
1124 executer = GetPostUploadExecuter()
1125 # The root presubmit file should be executed after the ones in subdirectories.
1126 # i.e. the specific post upload hooks should run before the general ones.
1127 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1128 presubmit_files.reverse()
1129
1130 for filename in presubmit_files:
1131 filename = os.path.abspath(filename)
1132 if verbose:
1133 output_stream.write("Running %s\n" % filename)
1134 # Accept CRLF presubmit script.
1135 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1136 results.extend(executer.ExecPresubmitScript(
1137 presubmit_script, filename, cl, change))
1138 output_stream.write('\n')
1139 if results:
1140 output_stream.write('** Post Upload Hook Messages **\n')
1141 for result in results:
1142 result.handle(output_stream)
1143 output_stream.write('\n')
1144
1145 return results
1146
1147
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001148class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001149 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001150 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001151 """
1152 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001153 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001154 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001155 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001156 gerrit_obj: provides basic Gerrit codereview functionality.
1157 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001158 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001159 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001160 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001161 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001162 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001163 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001164 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001165
1166 def ExecPresubmitScript(self, script_text, presubmit_path):
1167 """Executes a single presubmit script.
1168
1169 Args:
1170 script_text: The text of the presubmit script.
1171 presubmit_path: The path to the presubmit file (this will be reported via
1172 input_api.PresubmitLocalPath()).
1173
1174 Return:
1175 A list of result objects, empty if no problems.
1176 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001177
chase@chromium.org8e416c82009-10-06 04:30:44 +00001178 # Change to the presubmit file's directory to support local imports.
1179 main_path = os.getcwd()
1180 os.chdir(os.path.dirname(presubmit_path))
1181
1182 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001183 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001184 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001185 gerrit_obj=self.gerrit, dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001186 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001187 try:
1188 exec script_text in context
1189 except Exception, e:
1190 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001191
1192 # These function names must change if we make substantial changes to
1193 # the presubmit API that are not backwards compatible.
1194 if self.committing:
1195 function_name = 'CheckChangeOnCommit'
1196 else:
1197 function_name = 'CheckChangeOnUpload'
1198 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001199 context['__args'] = (input_api, OutputApi(self.committing))
tobiasjs2836bcf2016-08-16 04:08:16 -07001200 logging.debug('Running %s in %s', function_name, presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001201 result = eval(function_name + '(*__args)', context)
tobiasjs2836bcf2016-08-16 04:08:16 -07001202 logging.debug('Running %s done.', function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001203 if not (isinstance(result, types.TupleType) or
1204 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001205 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001206 'Presubmit functions must return a tuple or list')
1207 for item in result:
1208 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001209 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001210 'All presubmit results must be of types derived from '
1211 'output_api.PresubmitResult')
1212 else:
1213 result = () # no error since the script doesn't care about current event.
1214
scottmg86099d72016-09-01 09:16:51 -07001215 input_api.ShutdownPool()
1216
chase@chromium.org8e416c82009-10-06 04:30:44 +00001217 # Return the process to the original working directory.
1218 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001219 return result
1220
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001221def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001222 committing,
1223 verbose,
1224 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001225 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001226 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001227 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001228 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001229 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001230 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001231 """Runs all presubmit checks that apply to the files in the change.
1232
1233 This finds all PRESUBMIT.py files in directories enclosing the files in the
1234 change (up to the repository root) and calls the relevant entrypoint function
1235 depending on whether the change is being committed or uploaded.
1236
1237 Prints errors, warnings and notifications. Prompts the user for warnings
1238 when needed.
1239
1240 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001241 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001242 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001243 verbose: Prints debug info.
1244 output_stream: A stream to write output from presubmit tests to.
1245 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001246 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001247 may_prompt: Enable (y/n) questions on warning or error. If False,
1248 any questions are answered with yes by default.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001249 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001250 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001251 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001252
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001253 Warning:
1254 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1255 SHOULD be sys.stdin.
1256
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001257 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001258 A PresubmitOutput object. Use output.should_continue() to figure out
1259 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001260 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001261 old_environ = os.environ
1262 try:
1263 # Make sure python subprocesses won't generate .pyc files.
1264 os.environ = os.environ.copy()
1265 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001266
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001267 output = PresubmitOutput(input_stream, output_stream)
1268 if committing:
1269 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001270 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001271 output.write("Running presubmit upload checks ...\n")
1272 start_time = time.time()
1273 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001274 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001275 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001276 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001277 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001278 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001279 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001280 if default_presubmit:
1281 if verbose:
1282 output.write("Running default presubmit script.\n")
1283 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1284 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1285 for filename in presubmit_files:
1286 filename = os.path.abspath(filename)
1287 if verbose:
1288 output.write("Running %s\n" % filename)
1289 # Accept CRLF presubmit script.
1290 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1291 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001292
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001293 errors = []
1294 notifications = []
1295 warnings = []
1296 for result in results:
1297 if result.fatal:
1298 errors.append(result)
1299 elif result.should_prompt:
1300 warnings.append(result)
1301 else:
1302 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001303
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001304 output.write('\n')
1305 for name, items in (('Messages', notifications),
1306 ('Warnings', warnings),
1307 ('ERRORS', errors)):
1308 if items:
1309 output.write('** Presubmit %s **\n' % name)
1310 for item in items:
1311 item.handle(output)
1312 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001313
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001314 total_time = time.time() - start_time
1315 if total_time > 1.0:
1316 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001317
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001318 if errors:
1319 output.fail()
1320 elif warnings:
1321 output.write('There were presubmit warnings. ')
1322 if may_prompt:
1323 output.prompt_yes_no('Are you sure you wish to continue? (y/N): ')
1324 else:
1325 output.write('Presubmit checks passed.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001326
1327 global _ASKED_FOR_FEEDBACK
1328 # Ask for feedback one time out of 5.
1329 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001330 output.write(
1331 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1332 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1333 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001334 _ASKED_FOR_FEEDBACK = True
1335 return output
1336 finally:
1337 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001338
1339
1340def ScanSubDirs(mask, recursive):
1341 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001342 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001343
1344 results = []
1345 for root, dirs, files in os.walk('.'):
1346 if '.svn' in dirs:
1347 dirs.remove('.svn')
1348 if '.git' in dirs:
1349 dirs.remove('.git')
1350 for name in files:
1351 if fnmatch.fnmatch(name, mask):
1352 results.append(os.path.join(root, name))
1353 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001354
1355
1356def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001357 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001358 files = []
1359 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001360 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001361 return files
1362
1363
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001364def load_files(options, args):
1365 """Tries to determine the SCM."""
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001366 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001367 if args:
1368 files = ParseFiles(args, options.recursive)
agable0b65e732016-11-22 09:25:46 -08001369 change_scm = scm.determine_scm(options.root)
1370 if change_scm == 'git':
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001371 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001372 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001373 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001374 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001375 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001376 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001377 if not files:
1378 return None, None
1379 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001380 return change_class, files
1381
1382
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001383class NonexistantCannedCheckFilter(Exception):
1384 pass
1385
1386
1387@contextlib.contextmanager
1388def canned_check_filter(method_names):
1389 filtered = {}
1390 try:
1391 for method_name in method_names:
1392 if not hasattr(presubmit_canned_checks, method_name):
1393 raise NonexistantCannedCheckFilter(method_name)
1394 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1395 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1396 yield
1397 finally:
1398 for name, method in filtered.iteritems():
1399 setattr(presubmit_canned_checks, name, method)
1400
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001401
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001402def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001403 """Runs an external program, potentially from a child process created by the
1404 multiprocessing module.
1405
1406 multiprocessing needs a top level function with a single argument.
1407 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001408 cmd_data.kwargs['stdout'] = subprocess.PIPE
1409 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1410 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001411 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001412 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001413 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001414 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001415 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001416 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001417 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1418 if code != 0:
1419 return cmd_data.message(
1420 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1421 if cmd_data.info:
1422 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001423
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001424
sbc@chromium.org013731e2015-02-26 18:28:43 +00001425def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001426 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001427 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001428 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001429 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001430 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1431 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001432 parser.add_option("-r", "--recursive", action="store_true",
1433 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001434 parser.add_option("-v", "--verbose", action="count", default=0,
1435 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001436 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001437 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001438 parser.add_option("--description", default='')
1439 parser.add_option("--issue", type='int', default=0)
1440 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001441 parser.add_option("--root", default=os.getcwd(),
1442 help="Search for PRESUBMIT.py up to this directory. "
1443 "If inherit-review-settings-ok is present in this "
1444 "directory, parent directories up to the root file "
1445 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001446 parser.add_option("--upstream",
1447 help="Git only: the base ref or upstream branch against "
1448 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001449 parser.add_option("--default_presubmit")
1450 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001451 parser.add_option("--skip_canned", action='append', default=[],
1452 help="A list of checks to skip which appear in "
1453 "presubmit_canned_checks. Can be provided multiple times "
1454 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001455 parser.add_option("--dry_run", action='store_true',
1456 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001457 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001458 parser.add_option("--gerrit_fetch", action='store_true',
1459 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001460 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1461 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001462 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1463 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001464 # These are for OAuth2 authentication for bots. See also apply_issue.py
1465 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1466 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1467
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001468 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001469 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001470 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001471
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001472 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001473 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001474 elif options.verbose:
1475 logging.basicConfig(level=logging.INFO)
1476 else:
1477 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001478
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001479 if (any((options.rietveld_url, options.rietveld_email_file,
1480 options.rietveld_fetch, options.rietveld_private_key_file))
1481 and any((options.gerrit_url, options.gerrit_fetch))):
1482 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1483 'allowed')
1484
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001485 if options.rietveld_email and options.rietveld_email_file:
1486 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1487 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001488 if options.rietveld_email_file:
1489 with open(options.rietveld_email_file, "rb") as f:
1490 options.rietveld_email = f.read().strip()
1491
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001492 change_class, files = load_files(options, args)
1493 if not change_class:
1494 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001495 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001496
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001497 rietveld_obj, gerrit_obj = None, None
1498
maruel@chromium.org239f4112011-06-03 20:08:23 +00001499 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001500 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001501 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001502 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1503 options.rietveld_url,
1504 options.rietveld_email,
1505 options.rietveld_private_key_file)
1506 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001507 rietveld_obj = rietveld.CachingRietveld(
1508 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001509 auth_config,
1510 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001511 if options.rietveld_fetch:
1512 assert options.issue
1513 props = rietveld_obj.get_issue_properties(options.issue, False)
1514 options.author = props['owner_email']
1515 options.description = props['description']
1516 logging.info('Got author: "%s"', options.author)
1517 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001518
1519 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001520 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001521 rietveld_obj = None
1522 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1523 options.author = gerrit_obj.GetChangeOwner(options.issue)
1524 options.description = gerrit_obj.GetChangeDescription(options.issue,
1525 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001526 logging.info('Got author: "%s"', options.author)
1527 logging.info('Got description: """\n%s\n"""', options.description)
1528
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001529 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001530 with canned_check_filter(options.skip_canned):
1531 results = DoPresubmitChecks(
1532 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001533 options.description,
1534 options.root,
1535 files,
1536 options.issue,
1537 options.patchset,
1538 options.author,
1539 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001540 options.commit,
1541 options.verbose,
1542 sys.stdout,
1543 sys.stdin,
1544 options.default_presubmit,
1545 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001546 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001547 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001548 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001549 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001550 except NonexistantCannedCheckFilter, e:
1551 print >> sys.stderr, (
1552 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1553 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001554 except PresubmitFailure, e:
1555 print >> sys.stderr, e
1556 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1557 print >> sys.stderr, 'If all fails, contact maruel@'
1558 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001559
1560
1561if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001562 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001563 try:
1564 sys.exit(main())
1565 except KeyboardInterrupt:
1566 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001567 sys.exit(2)