blob: ae7263a52b31c6ec774d769a724cc86c0979f6b3 [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.orgdbbeedc2009-05-22 20:26:17 +0000667 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
668 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000669 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000670
agable0b65e732016-11-22 09:25:46 -0800671 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000672 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000673
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000674 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000675 raise NotImplementedError() # Implement when needed
676
agable0b65e732016-11-22 09:25:46 -0800677 def IsTextFile(self):
678 """An alias to IsTestableFile for backwards compatibility."""
679 return self.IsTestableFile()
680
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700681 def OldContents(self):
682 """Returns an iterator over the lines in the old version of file.
683
Daniel Cheng2da34fe2017-03-21 20:42:12 -0700684 The old version is the file before any modifications in the user's
685 workspace, i.e. the "left hand side".
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700686
687 Contents will be empty if the file is a directory or does not exist.
688 Note: The carriage returns (LF or CR) are stripped off.
689 """
690 return self._diff_cache.GetOldContents(self.LocalPath(),
691 self._local_root).splitlines()
692
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000693 def NewContents(self):
694 """Returns an iterator over the lines in the new version of file.
695
696 The new version is the file in the user's workspace, i.e. the "right hand
697 side".
698
699 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000700 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000701 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000702 if self._cached_new_contents is None:
703 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800704 try:
705 self._cached_new_contents = gclient_utils.FileRead(
706 self.AbsoluteLocalPath(), 'rU').splitlines()
707 except IOError:
708 pass # File not found? That's fine; maybe it was deleted.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000709 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000710
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000711 def ChangedContents(self):
712 """Returns a list of tuples (line number, line text) of all new lines.
713
714 This relies on the scm diff output describing each changed code section
715 with a line of the form
716
717 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
718 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000719 if self._cached_changed_contents is not None:
720 return self._cached_changed_contents[:]
721 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000722 line_num = 0
723
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000724 for line in self.GenerateScmDiff().splitlines():
725 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
726 if m:
727 line_num = int(m.groups(1)[0])
728 continue
729 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000730 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000731 if not line.startswith('-'):
732 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000733 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000734
maruel@chromium.org5de13972009-06-10 18:16:06 +0000735 def __str__(self):
736 return self.LocalPath()
737
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000738 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000739 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000740
maruel@chromium.org58407af2011-04-12 23:15:57 +0000741
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000742class GitAffectedFile(AffectedFile):
743 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000744 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800745 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000746
nick@chromium.orgff526192013-06-10 19:30:26 +0000747 DIFF_CACHE = _GitDiffCache
748
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000749 def __init__(self, *args, **kwargs):
750 AffectedFile.__init__(self, *args, **kwargs)
751 self._server_path = None
agable0b65e732016-11-22 09:25:46 -0800752 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000753
agable0b65e732016-11-22 09:25:46 -0800754 def IsTestableFile(self):
755 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000756 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -0800757 # A deleted file is not testable.
758 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000759 else:
agable0b65e732016-11-22 09:25:46 -0800760 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
761 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000762
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000763
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000764class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000765 """Describe a change.
766
767 Used directly by the presubmit scripts to query the current change being
768 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000769
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000770 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000771 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000772 self.KEY: equivalent to tags['KEY']
773 """
774
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000775 _AFFECTED_FILES = AffectedFile
776
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000777 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000778 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000779 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000780 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000781
maruel@chromium.org58407af2011-04-12 23:15:57 +0000782 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000783 self, name, description, local_root, files, issue, patchset, author,
784 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000785 if files is None:
786 files = []
787 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000788 # Convert root into an absolute path.
789 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000790 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000791 self.issue = issue
792 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000793 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000794
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000795 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000796 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000797 self._description_without_tags = ''
798 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000799
maruel@chromium.orge085d812011-10-10 19:49:15 +0000800 assert all(
801 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
802
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000803 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000804 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000805 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
806 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000807 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000808
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000809 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000810 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000811 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000812
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000813 def DescriptionText(self):
814 """Returns the user-entered changelist description, minus tags.
815
816 Any line in the user-provided description starting with e.g. "FOO="
817 (whitespace permitted before and around) is considered a tag line. Such
818 lines are stripped out of the description this function returns.
819 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000820 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000821
822 def FullDescriptionText(self):
823 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000824 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000825
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000826 def SetDescriptionText(self, description):
827 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000828
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000829 Also updates the list of tags."""
830 self._full_description = description
831
832 # From the description text, build up a dictionary of key/value pairs
833 # plus the description minus all key/value or "tag" lines.
834 description_without_tags = []
835 self.tags = {}
836 for line in self._full_description.splitlines():
837 m = self.TAG_LINE_RE.match(line)
838 if m:
839 self.tags[m.group('key')] = m.group('value')
840 else:
841 description_without_tags.append(line)
842
843 # Change back to text and remove whitespace at end.
844 self._description_without_tags = (
845 '\n'.join(description_without_tags).rstrip())
846
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000847 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000848 """Returns the repository (checkout) root directory for this change,
849 as an absolute path.
850 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000851 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000852
853 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000854 """Return tags directly as attributes on the object."""
855 if not re.match(r"^[A-Z_]*$", attr):
856 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000857 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000858
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000859 def AllFiles(self, root=None):
860 """List all files under source control in the repo."""
861 raise NotImplementedError()
862
agable0b65e732016-11-22 09:25:46 -0800863 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000864 """Returns a list of AffectedFile instances for all files in the change.
865
866 Args:
867 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +0000868 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000869
870 Returns:
871 [AffectedFile(path, action), AffectedFile(path, action)]
872 """
agable0b65e732016-11-22 09:25:46 -0800873 affected = filter(file_filter, self._affected_files)
sail@chromium.org5538e022011-05-12 17:53:16 +0000874
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000875 if include_deletes:
876 return affected
877 else:
878 return filter(lambda x: x.Action() != 'D', affected)
879
agable0b65e732016-11-22 09:25:46 -0800880 def AffectedTestableFiles(self, include_deletes=None):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000881 """Return a list of the existing text files in a change."""
882 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800883 warn("AffectedTeestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000884 " is deprecated and ignored" % str(include_deletes),
885 category=DeprecationWarning,
886 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800887 return filter(lambda x: x.IsTestableFile(),
888 self.AffectedFiles(include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000889
agable0b65e732016-11-22 09:25:46 -0800890 def AffectedTextFiles(self, include_deletes=None):
891 """An alias to AffectedTestableFiles for backwards compatibility."""
892 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000893
agable0b65e732016-11-22 09:25:46 -0800894 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000895 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800896 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000897
agable0b65e732016-11-22 09:25:46 -0800898 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000899 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800900 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000901
902 def RightHandSideLines(self):
903 """An iterator over all text lines in "new" version of changed files.
904
905 Lists lines from new or modified text files in the change.
906
907 This is useful for doing line-by-line regex checks, like checking for
908 trailing whitespace.
909
910 Yields:
911 a 3 tuple:
912 the AffectedFile instance of the current file;
913 integer line number (1-based); and
914 the contents of the line as a string.
915 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000916 return _RightHandSideLinesImpl(
917 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -0800918 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000919
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000920
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000921class GitChange(Change):
922 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000923 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000924
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000925 def AllFiles(self, root=None):
926 """List all files under source control in the repo."""
927 root = root or self.RepositoryRoot()
928 return subprocess.check_output(
929 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
930
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000931
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000932def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000933 """Finds all presubmit files that apply to a given set of source files.
934
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000935 If inherit-review-settings-ok is present right under root, looks for
936 PRESUBMIT.py in directories enclosing root.
937
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000938 Args:
939 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000940 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000941
942 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000943 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000944 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000945 files = [normpath(os.path.join(root, f)) for f in files]
946
947 # List all the individual directories containing files.
948 directories = set([os.path.dirname(f) for f in files])
949
950 # Ignore root if inherit-review-settings-ok is present.
951 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
952 root = None
953
954 # Collect all unique directories that may contain PRESUBMIT.py.
955 candidates = set()
956 for directory in directories:
957 while True:
958 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000959 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000960 candidates.add(directory)
961 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000962 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000963 parent_dir = os.path.dirname(directory)
964 if parent_dir == directory:
965 # We hit the system root directory.
966 break
967 directory = parent_dir
968
969 # Look for PRESUBMIT.py in all candidate directories.
970 results = []
971 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -0700972 try:
973 for f in os.listdir(directory):
974 p = os.path.join(directory, f)
975 if os.path.isfile(p) and re.match(
976 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
977 results.append(p)
978 except OSError:
979 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000980
tobiasjs2836bcf2016-08-16 04:08:16 -0700981 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000982 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000983
984
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +0000985class GetTryMastersExecuter(object):
986 @staticmethod
987 def ExecPresubmitScript(script_text, presubmit_path, project, change):
988 """Executes GetPreferredTryMasters() from a single presubmit script.
989
990 Args:
991 script_text: The text of the presubmit script.
992 presubmit_path: Project script to run.
993 project: Project name to pass to presubmit script for bot selection.
994
995 Return:
996 A map of try masters to map of builders to set of tests.
997 """
998 context = {}
999 try:
1000 exec script_text in context
1001 except Exception, e:
1002 raise PresubmitFailure('"%s" had an exception.\n%s'
1003 % (presubmit_path, e))
1004
1005 function_name = 'GetPreferredTryMasters'
1006 if function_name not in context:
1007 return {}
1008 get_preferred_try_masters = context[function_name]
1009 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1010 raise PresubmitFailure(
1011 'Expected function "GetPreferredTryMasters" to take two arguments.')
1012 return get_preferred_try_masters(project, change)
1013
1014
rmistry@google.com5626a922015-02-26 14:03:30 +00001015class GetPostUploadExecuter(object):
1016 @staticmethod
1017 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1018 """Executes PostUploadHook() from a single presubmit script.
1019
1020 Args:
1021 script_text: The text of the presubmit script.
1022 presubmit_path: Project script to run.
1023 cl: The Changelist object.
1024 change: The Change object.
1025
1026 Return:
1027 A list of results objects.
1028 """
1029 context = {}
1030 try:
1031 exec script_text in context
1032 except Exception, e:
1033 raise PresubmitFailure('"%s" had an exception.\n%s'
1034 % (presubmit_path, e))
1035
1036 function_name = 'PostUploadHook'
1037 if function_name not in context:
1038 return {}
1039 post_upload_hook = context[function_name]
1040 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1041 raise PresubmitFailure(
1042 'Expected function "PostUploadHook" to take three arguments.')
1043 return post_upload_hook(cl, change, OutputApi(False))
1044
1045
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001046def _MergeMasters(masters1, masters2):
1047 """Merges two master maps. Merges also the tests of each builder."""
1048 result = {}
1049 for (master, builders) in itertools.chain(masters1.iteritems(),
1050 masters2.iteritems()):
1051 new_builders = result.setdefault(master, {})
1052 for (builder, tests) in builders.iteritems():
1053 new_builders.setdefault(builder, set([])).update(tests)
1054 return result
1055
1056
1057def DoGetTryMasters(change,
1058 changed_files,
1059 repository_root,
1060 default_presubmit,
1061 project,
1062 verbose,
1063 output_stream):
1064 """Get the list of try masters from the presubmit scripts.
1065
1066 Args:
1067 changed_files: List of modified files.
1068 repository_root: The repository root.
1069 default_presubmit: A default presubmit script to execute in any case.
1070 project: Optional name of a project used in selecting trybots.
1071 verbose: Prints debug info.
1072 output_stream: A stream to write debug output to.
1073
1074 Return:
1075 Map of try masters to map of builders to set of tests.
1076 """
1077 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1078 if not presubmit_files and verbose:
1079 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1080 results = {}
1081 executer = GetTryMastersExecuter()
1082
1083 if default_presubmit:
1084 if verbose:
1085 output_stream.write("Running default presubmit script.\n")
1086 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1087 results = _MergeMasters(results, executer.ExecPresubmitScript(
1088 default_presubmit, fake_path, project, change))
1089 for filename in presubmit_files:
1090 filename = os.path.abspath(filename)
1091 if verbose:
1092 output_stream.write("Running %s\n" % filename)
1093 # Accept CRLF presubmit script.
1094 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1095 results = _MergeMasters(results, executer.ExecPresubmitScript(
1096 presubmit_script, filename, project, change))
1097
1098 # Make sets to lists again for later JSON serialization.
1099 for builders in results.itervalues():
1100 for builder in builders:
1101 builders[builder] = list(builders[builder])
1102
1103 if results and verbose:
1104 output_stream.write('%s\n' % str(results))
1105 return results
1106
1107
rmistry@google.com5626a922015-02-26 14:03:30 +00001108def DoPostUploadExecuter(change,
1109 cl,
1110 repository_root,
1111 verbose,
1112 output_stream):
1113 """Execute the post upload hook.
1114
1115 Args:
1116 change: The Change object.
1117 cl: The Changelist object.
1118 repository_root: The repository root.
1119 verbose: Prints debug info.
1120 output_stream: A stream to write debug output to.
1121 """
1122 presubmit_files = ListRelevantPresubmitFiles(
1123 change.LocalPaths(), repository_root)
1124 if not presubmit_files and verbose:
1125 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1126 results = []
1127 executer = GetPostUploadExecuter()
1128 # The root presubmit file should be executed after the ones in subdirectories.
1129 # i.e. the specific post upload hooks should run before the general ones.
1130 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1131 presubmit_files.reverse()
1132
1133 for filename in presubmit_files:
1134 filename = os.path.abspath(filename)
1135 if verbose:
1136 output_stream.write("Running %s\n" % filename)
1137 # Accept CRLF presubmit script.
1138 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1139 results.extend(executer.ExecPresubmitScript(
1140 presubmit_script, filename, cl, change))
1141 output_stream.write('\n')
1142 if results:
1143 output_stream.write('** Post Upload Hook Messages **\n')
1144 for result in results:
1145 result.handle(output_stream)
1146 output_stream.write('\n')
1147
1148 return results
1149
1150
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001151class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001152 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001153 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001154 """
1155 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001156 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001157 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001158 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001159 gerrit_obj: provides basic Gerrit codereview functionality.
1160 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001161 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001162 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001163 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001164 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001165 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001166 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001167 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001168
1169 def ExecPresubmitScript(self, script_text, presubmit_path):
1170 """Executes a single presubmit script.
1171
1172 Args:
1173 script_text: The text of the presubmit script.
1174 presubmit_path: The path to the presubmit file (this will be reported via
1175 input_api.PresubmitLocalPath()).
1176
1177 Return:
1178 A list of result objects, empty if no problems.
1179 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001180
chase@chromium.org8e416c82009-10-06 04:30:44 +00001181 # Change to the presubmit file's directory to support local imports.
1182 main_path = os.getcwd()
1183 os.chdir(os.path.dirname(presubmit_path))
1184
1185 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001186 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001187 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001188 gerrit_obj=self.gerrit, dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001189 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001190 try:
1191 exec script_text in context
1192 except Exception, e:
1193 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001194
1195 # These function names must change if we make substantial changes to
1196 # the presubmit API that are not backwards compatible.
1197 if self.committing:
1198 function_name = 'CheckChangeOnCommit'
1199 else:
1200 function_name = 'CheckChangeOnUpload'
1201 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001202 context['__args'] = (input_api, OutputApi(self.committing))
tobiasjs2836bcf2016-08-16 04:08:16 -07001203 logging.debug('Running %s in %s', function_name, presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001204 result = eval(function_name + '(*__args)', context)
tobiasjs2836bcf2016-08-16 04:08:16 -07001205 logging.debug('Running %s done.', function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001206 if not (isinstance(result, types.TupleType) or
1207 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001208 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001209 'Presubmit functions must return a tuple or list')
1210 for item in result:
1211 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001212 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001213 'All presubmit results must be of types derived from '
1214 'output_api.PresubmitResult')
1215 else:
1216 result = () # no error since the script doesn't care about current event.
1217
scottmg86099d72016-09-01 09:16:51 -07001218 input_api.ShutdownPool()
1219
chase@chromium.org8e416c82009-10-06 04:30:44 +00001220 # Return the process to the original working directory.
1221 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001222 return result
1223
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001224def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001225 committing,
1226 verbose,
1227 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001228 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001229 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001230 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001231 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001232 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001233 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001234 """Runs all presubmit checks that apply to the files in the change.
1235
1236 This finds all PRESUBMIT.py files in directories enclosing the files in the
1237 change (up to the repository root) and calls the relevant entrypoint function
1238 depending on whether the change is being committed or uploaded.
1239
1240 Prints errors, warnings and notifications. Prompts the user for warnings
1241 when needed.
1242
1243 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001244 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001245 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001246 verbose: Prints debug info.
1247 output_stream: A stream to write output from presubmit tests to.
1248 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001249 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001250 may_prompt: Enable (y/n) questions on warning or error. If False,
1251 any questions are answered with yes by default.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001252 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001253 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001254 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001255
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001256 Warning:
1257 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1258 SHOULD be sys.stdin.
1259
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001260 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001261 A PresubmitOutput object. Use output.should_continue() to figure out
1262 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001263 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001264 old_environ = os.environ
1265 try:
1266 # Make sure python subprocesses won't generate .pyc files.
1267 os.environ = os.environ.copy()
1268 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001269
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001270 output = PresubmitOutput(input_stream, output_stream)
1271 if committing:
1272 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001273 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001274 output.write("Running presubmit upload checks ...\n")
1275 start_time = time.time()
1276 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001277 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001278 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001279 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001280 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001281 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001282 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001283 if default_presubmit:
1284 if verbose:
1285 output.write("Running default presubmit script.\n")
1286 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1287 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1288 for filename in presubmit_files:
1289 filename = os.path.abspath(filename)
1290 if verbose:
1291 output.write("Running %s\n" % filename)
1292 # Accept CRLF presubmit script.
1293 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1294 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001295
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001296 errors = []
1297 notifications = []
1298 warnings = []
1299 for result in results:
1300 if result.fatal:
1301 errors.append(result)
1302 elif result.should_prompt:
1303 warnings.append(result)
1304 else:
1305 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001306
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001307 output.write('\n')
1308 for name, items in (('Messages', notifications),
1309 ('Warnings', warnings),
1310 ('ERRORS', errors)):
1311 if items:
1312 output.write('** Presubmit %s **\n' % name)
1313 for item in items:
1314 item.handle(output)
1315 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001316
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001317 total_time = time.time() - start_time
1318 if total_time > 1.0:
1319 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001320
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001321 if errors:
1322 output.fail()
1323 elif warnings:
1324 output.write('There were presubmit warnings. ')
1325 if may_prompt:
1326 output.prompt_yes_no('Are you sure you wish to continue? (y/N): ')
1327 else:
1328 output.write('Presubmit checks passed.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001329
1330 global _ASKED_FOR_FEEDBACK
1331 # Ask for feedback one time out of 5.
1332 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001333 output.write(
1334 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1335 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1336 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001337 _ASKED_FOR_FEEDBACK = True
1338 return output
1339 finally:
1340 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001341
1342
1343def ScanSubDirs(mask, recursive):
1344 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001345 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001346 else:
1347 results = []
1348 for root, dirs, files in os.walk('.'):
1349 if '.svn' in dirs:
1350 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001351 if '.git' in dirs:
1352 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001353 for name in files:
1354 if fnmatch.fnmatch(name, mask):
1355 results.append(os.path.join(root, name))
1356 return results
1357
1358
1359def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001360 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001361 files = []
1362 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001363 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001364 return files
1365
1366
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001367def load_files(options, args):
1368 """Tries to determine the SCM."""
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001369 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001370 if args:
1371 files = ParseFiles(args, options.recursive)
agable0b65e732016-11-22 09:25:46 -08001372 change_scm = scm.determine_scm(options.root)
1373 if change_scm == 'git':
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001374 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001375 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001376 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001377 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001378 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001379 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001380 if not files:
1381 return None, None
1382 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001383 return change_class, files
1384
1385
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001386class NonexistantCannedCheckFilter(Exception):
1387 pass
1388
1389
1390@contextlib.contextmanager
1391def canned_check_filter(method_names):
1392 filtered = {}
1393 try:
1394 for method_name in method_names:
1395 if not hasattr(presubmit_canned_checks, method_name):
1396 raise NonexistantCannedCheckFilter(method_name)
1397 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1398 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1399 yield
1400 finally:
1401 for name, method in filtered.iteritems():
1402 setattr(presubmit_canned_checks, name, method)
1403
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001404
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001405def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001406 """Runs an external program, potentially from a child process created by the
1407 multiprocessing module.
1408
1409 multiprocessing needs a top level function with a single argument.
1410 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001411 cmd_data.kwargs['stdout'] = subprocess.PIPE
1412 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1413 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001414 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001415 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001416 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001417 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001418 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001419 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001420 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1421 if code != 0:
1422 return cmd_data.message(
1423 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1424 if cmd_data.info:
1425 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001426
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001427
sbc@chromium.org013731e2015-02-26 18:28:43 +00001428def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001429 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001430 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001431 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001432 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001433 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1434 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001435 parser.add_option("-r", "--recursive", action="store_true",
1436 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001437 parser.add_option("-v", "--verbose", action="count", default=0,
1438 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001439 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001440 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001441 parser.add_option("--description", default='')
1442 parser.add_option("--issue", type='int', default=0)
1443 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001444 parser.add_option("--root", default=os.getcwd(),
1445 help="Search for PRESUBMIT.py up to this directory. "
1446 "If inherit-review-settings-ok is present in this "
1447 "directory, parent directories up to the root file "
1448 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001449 parser.add_option("--upstream",
1450 help="Git only: the base ref or upstream branch against "
1451 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001452 parser.add_option("--default_presubmit")
1453 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001454 parser.add_option("--skip_canned", action='append', default=[],
1455 help="A list of checks to skip which appear in "
1456 "presubmit_canned_checks. Can be provided multiple times "
1457 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001458 parser.add_option("--dry_run", action='store_true',
1459 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001460 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001461 parser.add_option("--gerrit_fetch", action='store_true',
1462 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001463 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1464 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001465 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1466 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001467 # These are for OAuth2 authentication for bots. See also apply_issue.py
1468 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1469 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1470
phajdan.jr@chromium.org2ff30182016-03-23 09:52:51 +00001471 # TODO(phajdan.jr): Update callers and remove obsolete --trybot-json .
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001472 parser.add_option("--trybot-json",
1473 help="Output trybot information to the file specified.")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001474 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001475 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001476 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001477
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001478 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001479 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001480 elif options.verbose:
1481 logging.basicConfig(level=logging.INFO)
1482 else:
1483 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001484
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001485 if (any((options.rietveld_url, options.rietveld_email_file,
1486 options.rietveld_fetch, options.rietveld_private_key_file))
1487 and any((options.gerrit_url, options.gerrit_fetch))):
1488 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1489 'allowed')
1490
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001491 if options.rietveld_email and options.rietveld_email_file:
1492 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1493 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001494 if options.rietveld_email_file:
1495 with open(options.rietveld_email_file, "rb") as f:
1496 options.rietveld_email = f.read().strip()
1497
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001498 change_class, files = load_files(options, args)
1499 if not change_class:
1500 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001501 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001502
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001503 rietveld_obj, gerrit_obj = None, None
1504
maruel@chromium.org239f4112011-06-03 20:08:23 +00001505 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001506 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001507 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001508 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1509 options.rietveld_url,
1510 options.rietveld_email,
1511 options.rietveld_private_key_file)
1512 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001513 rietveld_obj = rietveld.CachingRietveld(
1514 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001515 auth_config,
1516 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001517 if options.rietveld_fetch:
1518 assert options.issue
1519 props = rietveld_obj.get_issue_properties(options.issue, False)
1520 options.author = props['owner_email']
1521 options.description = props['description']
1522 logging.info('Got author: "%s"', options.author)
1523 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001524
1525 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001526 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001527 rietveld_obj = None
1528 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1529 options.author = gerrit_obj.GetChangeOwner(options.issue)
1530 options.description = gerrit_obj.GetChangeDescription(options.issue,
1531 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001532 logging.info('Got author: "%s"', options.author)
1533 logging.info('Got description: """\n%s\n"""', options.description)
1534
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001535 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001536 with canned_check_filter(options.skip_canned):
1537 results = DoPresubmitChecks(
1538 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001539 options.description,
1540 options.root,
1541 files,
1542 options.issue,
1543 options.patchset,
1544 options.author,
1545 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001546 options.commit,
1547 options.verbose,
1548 sys.stdout,
1549 sys.stdin,
1550 options.default_presubmit,
1551 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001552 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001553 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001554 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001555 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001556 except NonexistantCannedCheckFilter, e:
1557 print >> sys.stderr, (
1558 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1559 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001560 except PresubmitFailure, e:
1561 print >> sys.stderr, e
1562 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1563 print >> sys.stderr, 'If all fails, contact maruel@'
1564 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001565
1566
1567if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001568 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001569 try:
1570 sys.exit(main())
1571 except KeyboardInterrupt:
1572 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001573 sys.exit(2)