blob: 856f1108869df8a4180ee84ca5f18275d9edcda4 [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
Jochen Eisinger76f5fc62017-04-07 16:27:46 +020048import owners_finder
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000049import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000050import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000051import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000052import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053
54
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000055# Ask for feedback only once in program lifetime.
56_ASKED_FOR_FEEDBACK = False
57
58
maruel@chromium.org899e1c12011-04-07 17:03:18 +000059class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000060 pass
61
62
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000063class CommandData(object):
64 def __init__(self, name, cmd, kwargs, message):
65 self.name = name
66 self.cmd = cmd
67 self.kwargs = kwargs
68 self.message = message
69 self.info = None
70
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000071
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000072def normpath(path):
73 '''Version of os.path.normpath that also changes backward slashes to
74 forward slashes when not running on Windows.
75 '''
76 # This is safe to always do because the Windows version of os.path.normpath
77 # will replace forward slashes with backward slashes.
78 path = path.replace(os.sep, '/')
79 return os.path.normpath(path)
80
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000081
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000082def _RightHandSideLinesImpl(affected_files):
83 """Implements RightHandSideLines for InputApi and GclChange."""
84 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000085 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000086 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000087 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000088
89
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000090class PresubmitOutput(object):
91 def __init__(self, input_stream=None, output_stream=None):
92 self.input_stream = input_stream
93 self.output_stream = output_stream
94 self.reviewers = []
95 self.written_output = []
96 self.error_count = 0
97
98 def prompt_yes_no(self, prompt_string):
99 self.write(prompt_string)
100 if self.input_stream:
101 response = self.input_stream.readline().strip().lower()
102 if response not in ('y', 'yes'):
103 self.fail()
104 else:
105 self.fail()
106
107 def fail(self):
108 self.error_count += 1
109
110 def should_continue(self):
111 return not self.error_count
112
113 def write(self, s):
114 self.written_output.append(s)
115 if self.output_stream:
116 self.output_stream.write(s)
117
118 def getvalue(self):
119 return ''.join(self.written_output)
120
121
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000122# Top level object so multiprocessing can pickle
123# Public access through OutputApi object.
124class _PresubmitResult(object):
125 """Base class for result objects."""
126 fatal = False
127 should_prompt = False
128
129 def __init__(self, message, items=None, long_text=''):
130 """
131 message: A short one-line message to indicate errors.
132 items: A list of short strings to indicate where errors occurred.
133 long_text: multi-line text output, e.g. from another tool
134 """
135 self._message = message
136 self._items = items or []
137 if items:
138 self._items = items
139 self._long_text = long_text.rstrip()
140
141 def handle(self, output):
142 output.write(self._message)
143 output.write('\n')
144 for index, item in enumerate(self._items):
145 output.write(' ')
146 # Write separately in case it's unicode.
147 output.write(str(item))
148 if index < len(self._items) - 1:
149 output.write(' \\')
150 output.write('\n')
151 if self._long_text:
152 output.write('\n***************\n')
153 # Write separately in case it's unicode.
154 output.write(self._long_text)
155 output.write('\n***************\n')
156 if self.fatal:
157 output.fail()
158
159
160# Top level object so multiprocessing can pickle
161# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000162class _PresubmitError(_PresubmitResult):
163 """A hard presubmit error."""
164 fatal = True
165
166
167# Top level object so multiprocessing can pickle
168# Public access through OutputApi object.
169class _PresubmitPromptWarning(_PresubmitResult):
170 """An warning that prompts the user if they want to continue."""
171 should_prompt = True
172
173
174# Top level object so multiprocessing can pickle
175# Public access through OutputApi object.
176class _PresubmitNotifyResult(_PresubmitResult):
177 """Just print something to the screen -- but it's not even a warning."""
178 pass
179
180
181# Top level object so multiprocessing can pickle
182# Public access through OutputApi object.
183class _MailTextResult(_PresubmitResult):
184 """A warning that should be included in the review request email."""
185 def __init__(self, *args, **kwargs):
186 super(_MailTextResult, self).__init__()
187 raise NotImplementedError()
188
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000189class GerritAccessor(object):
190 """Limited Gerrit functionality for canned presubmit checks to work.
191
192 To avoid excessive Gerrit calls, caches the results.
193 """
194
195 def __init__(self, host):
196 self.host = host
197 self.cache = {}
198
199 def _FetchChangeDetail(self, issue):
200 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100201 try:
202 return gerrit_util.GetChangeDetail(
203 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700204 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100205 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:
Kenneth Russelldf6e7342017-04-24 17:07:41 -0700302 # If we're adding a new CQ_INCLUDE_TRYBOTS line then make
303 # absolutely sure to add it before any Change-Id: line, to avoid
304 # breaking Gerrit.
305 #
306 # The use of \n outside the capture group causes the last
307 # newline before Change-Id and any extra newlines after it to be
308 # consumed. They are re-added during the join operation.
309 #
310 # The filter operation drops the trailing empty string after the
311 # original string is split.
312 split_desc = filter(
313 None, re.split('\n(Change-Id: \w*)\n*', description, 1, re.M))
314 # Make sure to insert this before the last entry. For backward
315 # compatibility, ensure the CL description ends in a newline.
316 if len(split_desc) == 1:
317 insert_idx = 1
318 else:
319 insert_idx = len(split_desc) - 1
320 split_desc.insert(insert_idx, new_include_trybots)
321 new_description = '\n'.join(split_desc) + '\n'
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800322 cl.UpdateDescription(new_description, force=True)
323 return [self.PresubmitNotifyResult(message)]
324
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000325
326class InputApi(object):
327 """An instance of this object is passed to presubmit scripts so they can
328 know stuff about the change they're looking at.
329 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000330 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800331 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000332
maruel@chromium.org3410d912009-06-09 20:56:16 +0000333 # File extensions that are considered source files from a style guide
334 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000335 #
336 # Files without an extension aren't included in the list. If you want to
337 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
338 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000339 DEFAULT_WHITE_LIST = (
340 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000341 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
342 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000343 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000344 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000345 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000346 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000347 )
348
349 # Path regexp that should be excluded from being considered containing source
350 # files. Don't modify this list from a presubmit script!
351 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000352 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000353 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000354 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
355 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000356 # Output directories (just in case)
357 r".*\bDebug[\\\/].*",
358 r".*\bRelease[\\\/].*",
359 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000360 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000361 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000362 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000363 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000364 r"(|.*[\\\/])\.git[\\\/].*",
365 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000366 # There is no point in processing a patch file.
367 r".+\.diff$",
368 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000369 )
370
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000371 def __init__(self, change, presubmit_path, is_committing,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000372 rietveld_obj, verbose, gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000373 """Builds an InputApi object.
374
375 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000376 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000377 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000378 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000379 rietveld_obj: rietveld.Rietveld client object
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000380 gerrit_obj: provides basic Gerrit codereview functionality.
381 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000382 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000383 # Version number of the presubmit_support script.
384 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000385 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000386 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000387 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000388 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000389 self.dry_run = dry_run
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000390 # TBD
391 self.host_url = 'http://codereview.chromium.org'
392 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000393 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000394
395 # We expose various modules and functions as attributes of the input_api
396 # so that presubmit scripts don't have to import them.
397 self.basename = os.path.basename
398 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000399 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000400 self.cStringIO = cStringIO
dcheng091b7db2016-06-16 01:27:51 -0700401 self.fnmatch = fnmatch
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000402 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000403 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000404 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000405 self.os_listdir = os.listdir
406 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000407 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000408 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000409 self.pickle = pickle
410 self.marshal = marshal
411 self.re = re
412 self.subprocess = subprocess
413 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000414 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000415 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000416 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000417 self.urllib2 = urllib2
418
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000419 # To easily fork python.
420 self.python_executable = sys.executable
421 self.environ = os.environ
422
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000423 # InputApi.platform is the platform you're currently running on.
424 self.platform = sys.platform
425
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000426 self.cpu_count = multiprocessing.cpu_count()
427
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000428 # this is done here because in RunTests, the current working directory has
429 # changed, which causes Pool() to explode fantastically when run on windows
430 # (because it tries to load the __main__ module, which imports lots of
431 # things relative to the current working directory).
432 self._run_tests_pool = multiprocessing.Pool(self.cpu_count)
433
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000434 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000435 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000436
437 # We carry the canned checks so presubmit scripts can easily use them.
438 self.canned_checks = presubmit_canned_checks
439
Jochen Eisinger72606f82017-04-04 10:44:18 +0200440
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000441 # TODO(dpranke): figure out a list of all approved owners for a repo
442 # in order to be able to handle wildcard OWNERS files?
443 self.owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +0200444 fopen=file, os_path=self.os_path)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200445 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000446 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000447 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000448
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000449 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000450 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000451 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800452 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000453 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000454 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000455 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
456 for (a, b, header) in cpplint._re_pattern_templates
457 ]
458
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000459 def PresubmitLocalPath(self):
460 """Returns the local path of the presubmit script currently being run.
461
462 This is useful if you don't want to hard-code absolute paths in the
463 presubmit script. For example, It can be used to find another file
464 relative to the PRESUBMIT.py script, so the whole tree can be branched and
465 the presubmit script still works, without editing its content.
466 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000467 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000468
agable0b65e732016-11-22 09:25:46 -0800469 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000470 """Same as input_api.change.AffectedFiles() except only lists files
471 (and optionally directories) in the same directory as the current presubmit
472 script, or subdirectories thereof.
473 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000474 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000475 if len(dir_with_slash) == 1:
476 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000477
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000478 return filter(
479 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
agable0b65e732016-11-22 09:25:46 -0800480 self.change.AffectedFiles(include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000481
agable0b65e732016-11-22 09:25:46 -0800482 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000483 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800484 paths = [af.LocalPath() for af in self.AffectedFiles()]
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000485 logging.debug("LocalPaths: %s", paths)
486 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000487
agable0b65e732016-11-22 09:25:46 -0800488 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000489 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800490 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000491
agable0b65e732016-11-22 09:25:46 -0800492 def AffectedTestableFiles(self, include_deletes=None):
493 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000494 in the same directory as the current presubmit script, or subdirectories
495 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000496 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000497 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800498 warn("AffectedTestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000499 " is deprecated and ignored" % str(include_deletes),
500 category=DeprecationWarning,
501 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800502 return filter(lambda x: x.IsTestableFile(),
503 self.AffectedFiles(include_deletes=False))
504
505 def AffectedTextFiles(self, include_deletes=None):
506 """An alias to AffectedTestableFiles for backwards compatibility."""
507 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000508
maruel@chromium.org3410d912009-06-09 20:56:16 +0000509 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
510 """Filters out files that aren't considered "source file".
511
512 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
513 and InputApi.DEFAULT_BLACK_LIST is used respectively.
514
515 The lists will be compiled as regular expression and
516 AffectedFile.LocalPath() needs to pass both list.
517
518 Note: Copy-paste this function to suit your needs or use a lambda function.
519 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000520 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000521 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000522 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000523 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000524 return True
525 return False
526 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
527 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
528
529 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800530 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000531
532 If source_file is None, InputApi.FilterSourceFile() is used.
533 """
534 if not source_file:
535 source_file = self.FilterSourceFile
agable0b65e732016-11-22 09:25:46 -0800536 return filter(source_file, self.AffectedTestableFiles())
maruel@chromium.org3410d912009-06-09 20:56:16 +0000537
538 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000539 """An iterator over all text lines in "new" version of changed files.
540
541 Only lists lines from new or modified text files in the change that are
542 contained by the directory of the currently executing presubmit script.
543
544 This is useful for doing line-by-line regex checks, like checking for
545 trailing whitespace.
546
547 Yields:
548 a 3 tuple:
549 the AffectedFile instance of the current file;
550 integer line number (1-based); and
551 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000552
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000553 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000554 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000555 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000556 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000557
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000558 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000559 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000560
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000561 Deny reading anything outside the repository.
562 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000563 if isinstance(file_item, AffectedFile):
564 file_item = file_item.AbsoluteLocalPath()
565 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000566 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000567 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000568
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000569 @property
570 def tbr(self):
571 """Returns if a change is TBR'ed."""
572 return 'TBR' in self.change.tags
573
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000574 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000575 tests = []
576 msgs = []
577 for t in tests_mix:
578 if isinstance(t, OutputApi.PresubmitResult):
579 msgs.append(t)
580 else:
581 assert issubclass(t.message, _PresubmitResult)
582 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000583 if self.verbose:
584 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000585 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000586 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000587 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000588 else:
589 msgs.extend(map(CallCommand, tests))
590 return [m for m in msgs if m]
591
scottmg86099d72016-09-01 09:16:51 -0700592 def ShutdownPool(self):
593 self._run_tests_pool.close()
594 self._run_tests_pool.join()
595 self._run_tests_pool = None
596
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000597
nick@chromium.orgff526192013-06-10 19:30:26 +0000598class _DiffCache(object):
599 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000600 def __init__(self, upstream=None):
601 """Stores the upstream revision against which all diffs will be computed."""
602 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000603
604 def GetDiff(self, path, local_root):
605 """Get the diff for a particular path."""
606 raise NotImplementedError()
607
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700608 def GetOldContents(self, path, local_root):
609 """Get the old version for a particular path."""
610 raise NotImplementedError()
611
nick@chromium.orgff526192013-06-10 19:30:26 +0000612
nick@chromium.orgff526192013-06-10 19:30:26 +0000613class _GitDiffCache(_DiffCache):
614 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000615 def __init__(self, upstream):
616 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000617 self._diffs_by_file = None
618
619 def GetDiff(self, path, local_root):
620 if not self._diffs_by_file:
621 # Compute a single diff for all files and parse the output; should
622 # with git this is much faster than computing one diff for each file.
623 diffs = {}
624
625 # Don't specify any filenames below, because there are command line length
626 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000627 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
628 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000629
630 # This regex matches the path twice, separated by a space. Note that
631 # filename itself may contain spaces.
632 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
633 current_diff = []
634 keep_line_endings = True
635 for x in unified_diff.splitlines(keep_line_endings):
636 match = file_marker.match(x)
637 if match:
638 # Marks the start of a new per-file section.
639 diffs[match.group('filename')] = current_diff = [x]
640 elif x.startswith('diff --git'):
641 raise PresubmitFailure('Unexpected diff line: %s' % x)
642 else:
643 current_diff.append(x)
644
645 self._diffs_by_file = dict(
646 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
647
648 if path not in self._diffs_by_file:
649 raise PresubmitFailure(
650 'Unified diff did not contain entry for file %s' % path)
651
652 return self._diffs_by_file[path]
653
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700654 def GetOldContents(self, path, local_root):
655 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
656
nick@chromium.orgff526192013-06-10 19:30:26 +0000657
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000658class AffectedFile(object):
659 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000660
661 DIFF_CACHE = _DiffCache
662
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000663 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800664 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000665 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000666 self._path = path
667 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000668 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000669 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000670 self._cached_changed_contents = None
671 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000672 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700673 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000674
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000675 def LocalPath(self):
676 """Returns the path of this file on the local disk relative to client root.
677 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000678 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000679
680 def AbsoluteLocalPath(self):
681 """Returns the absolute path of this file on the local disk.
682 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000683 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000685 def Action(self):
686 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000687 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000688
agable0b65e732016-11-22 09:25:46 -0800689 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000690 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000691
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000692 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000693 raise NotImplementedError() # Implement when needed
694
agable0b65e732016-11-22 09:25:46 -0800695 def IsTextFile(self):
696 """An alias to IsTestableFile for backwards compatibility."""
697 return self.IsTestableFile()
698
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700699 def OldContents(self):
700 """Returns an iterator over the lines in the old version of file.
701
Daniel Cheng2da34fe2017-03-21 20:42:12 -0700702 The old version is the file before any modifications in the user's
703 workspace, i.e. the "left hand side".
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700704
705 Contents will be empty if the file is a directory or does not exist.
706 Note: The carriage returns (LF or CR) are stripped off.
707 """
708 return self._diff_cache.GetOldContents(self.LocalPath(),
709 self._local_root).splitlines()
710
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000711 def NewContents(self):
712 """Returns an iterator over the lines in the new version of file.
713
714 The new version is the file in the user's workspace, i.e. the "right hand
715 side".
716
717 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000718 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000719 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000720 if self._cached_new_contents is None:
721 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800722 try:
723 self._cached_new_contents = gclient_utils.FileRead(
724 self.AbsoluteLocalPath(), 'rU').splitlines()
725 except IOError:
726 pass # File not found? That's fine; maybe it was deleted.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000727 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000728
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000729 def ChangedContents(self):
730 """Returns a list of tuples (line number, line text) of all new lines.
731
732 This relies on the scm diff output describing each changed code section
733 with a line of the form
734
735 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
736 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000737 if self._cached_changed_contents is not None:
738 return self._cached_changed_contents[:]
739 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000740 line_num = 0
741
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000742 for line in self.GenerateScmDiff().splitlines():
743 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
744 if m:
745 line_num = int(m.groups(1)[0])
746 continue
747 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000748 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000749 if not line.startswith('-'):
750 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000751 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000752
maruel@chromium.org5de13972009-06-10 18:16:06 +0000753 def __str__(self):
754 return self.LocalPath()
755
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000756 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000757 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000758
maruel@chromium.org58407af2011-04-12 23:15:57 +0000759
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000760class GitAffectedFile(AffectedFile):
761 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000762 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800763 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000764
nick@chromium.orgff526192013-06-10 19:30:26 +0000765 DIFF_CACHE = _GitDiffCache
766
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000767 def __init__(self, *args, **kwargs):
768 AffectedFile.__init__(self, *args, **kwargs)
769 self._server_path = None
agable0b65e732016-11-22 09:25:46 -0800770 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000771
agable0b65e732016-11-22 09:25:46 -0800772 def IsTestableFile(self):
773 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000774 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -0800775 # A deleted file is not testable.
776 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000777 else:
agable0b65e732016-11-22 09:25:46 -0800778 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
779 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000780
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000781
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000782class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000783 """Describe a change.
784
785 Used directly by the presubmit scripts to query the current change being
786 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000787
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000788 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000789 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000790 self.KEY: equivalent to tags['KEY']
791 """
792
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000793 _AFFECTED_FILES = AffectedFile
794
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000795 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000796 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000797 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000798 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000799
maruel@chromium.org58407af2011-04-12 23:15:57 +0000800 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000801 self, name, description, local_root, files, issue, patchset, author,
802 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000803 if files is None:
804 files = []
805 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000806 # Convert root into an absolute path.
807 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000808 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000809 self.issue = issue
810 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000811 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000812
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000813 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000814 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000815 self._description_without_tags = ''
816 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000817
maruel@chromium.orge085d812011-10-10 19:49:15 +0000818 assert all(
819 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
820
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000821 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000822 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000823 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
824 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000825 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000826
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000827 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000828 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000829 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000830
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000831 def DescriptionText(self):
832 """Returns the user-entered changelist description, minus tags.
833
834 Any line in the user-provided description starting with e.g. "FOO="
835 (whitespace permitted before and around) is considered a tag line. Such
836 lines are stripped out of the description this function returns.
837 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000838 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000839
840 def FullDescriptionText(self):
841 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000842 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000843
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000844 def SetDescriptionText(self, description):
845 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000846
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000847 Also updates the list of tags."""
848 self._full_description = description
849
850 # From the description text, build up a dictionary of key/value pairs
851 # plus the description minus all key/value or "tag" lines.
852 description_without_tags = []
853 self.tags = {}
854 for line in self._full_description.splitlines():
855 m = self.TAG_LINE_RE.match(line)
856 if m:
857 self.tags[m.group('key')] = m.group('value')
858 else:
859 description_without_tags.append(line)
860
861 # Change back to text and remove whitespace at end.
862 self._description_without_tags = (
863 '\n'.join(description_without_tags).rstrip())
864
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000865 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000866 """Returns the repository (checkout) root directory for this change,
867 as an absolute path.
868 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000869 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000870
871 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000872 """Return tags directly as attributes on the object."""
873 if not re.match(r"^[A-Z_]*$", attr):
874 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000875 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000876
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000877 def AllFiles(self, root=None):
878 """List all files under source control in the repo."""
879 raise NotImplementedError()
880
agable0b65e732016-11-22 09:25:46 -0800881 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000882 """Returns a list of AffectedFile instances for all files in the change.
883
884 Args:
885 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +0000886 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000887
888 Returns:
889 [AffectedFile(path, action), AffectedFile(path, action)]
890 """
agable0b65e732016-11-22 09:25:46 -0800891 affected = filter(file_filter, self._affected_files)
sail@chromium.org5538e022011-05-12 17:53:16 +0000892
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000893 if include_deletes:
894 return affected
Lei Zhang9611c4c2017-04-04 01:41:56 -0700895 return filter(lambda x: x.Action() != 'D', affected)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000896
agable0b65e732016-11-22 09:25:46 -0800897 def AffectedTestableFiles(self, include_deletes=None):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000898 """Return a list of the existing text files in a change."""
899 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800900 warn("AffectedTeestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000901 " is deprecated and ignored" % str(include_deletes),
902 category=DeprecationWarning,
903 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800904 return filter(lambda x: x.IsTestableFile(),
905 self.AffectedFiles(include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000906
agable0b65e732016-11-22 09:25:46 -0800907 def AffectedTextFiles(self, include_deletes=None):
908 """An alias to AffectedTestableFiles for backwards compatibility."""
909 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000910
agable0b65e732016-11-22 09:25:46 -0800911 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000912 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800913 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000914
agable0b65e732016-11-22 09:25:46 -0800915 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000916 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800917 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000918
919 def RightHandSideLines(self):
920 """An iterator over all text lines in "new" version of changed files.
921
922 Lists lines from new or modified text files in the change.
923
924 This is useful for doing line-by-line regex checks, like checking for
925 trailing whitespace.
926
927 Yields:
928 a 3 tuple:
929 the AffectedFile instance of the current file;
930 integer line number (1-based); and
931 the contents of the line as a string.
932 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000933 return _RightHandSideLinesImpl(
934 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -0800935 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000936
Jochen Eisingerd0573ec2017-04-13 10:55:06 +0200937 def OriginalOwnersFiles(self):
938 """A map from path names of affected OWNERS files to their old content."""
939 def owners_file_filter(f):
940 return 'OWNERS' in os.path.split(f.LocalPath())[1]
941 files = self.AffectedFiles(file_filter=owners_file_filter)
942 return dict([(f.LocalPath(), f.OldContents()) for f in files])
943
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000944
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000945class GitChange(Change):
946 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000947 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000948
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000949 def AllFiles(self, root=None):
950 """List all files under source control in the repo."""
951 root = root or self.RepositoryRoot()
952 return subprocess.check_output(
953 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
954
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000955
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000956def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000957 """Finds all presubmit files that apply to a given set of source files.
958
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000959 If inherit-review-settings-ok is present right under root, looks for
960 PRESUBMIT.py in directories enclosing root.
961
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000962 Args:
963 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000964 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000965
966 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000967 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000968 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000969 files = [normpath(os.path.join(root, f)) for f in files]
970
971 # List all the individual directories containing files.
972 directories = set([os.path.dirname(f) for f in files])
973
974 # Ignore root if inherit-review-settings-ok is present.
975 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
976 root = None
977
978 # Collect all unique directories that may contain PRESUBMIT.py.
979 candidates = set()
980 for directory in directories:
981 while True:
982 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000983 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000984 candidates.add(directory)
985 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000986 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000987 parent_dir = os.path.dirname(directory)
988 if parent_dir == directory:
989 # We hit the system root directory.
990 break
991 directory = parent_dir
992
993 # Look for PRESUBMIT.py in all candidate directories.
994 results = []
995 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -0700996 try:
997 for f in os.listdir(directory):
998 p = os.path.join(directory, f)
999 if os.path.isfile(p) and re.match(
1000 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1001 results.append(p)
1002 except OSError:
1003 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001004
tobiasjs2836bcf2016-08-16 04:08:16 -07001005 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001006 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001007
1008
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001009class GetTryMastersExecuter(object):
1010 @staticmethod
1011 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1012 """Executes GetPreferredTryMasters() from a single presubmit script.
1013
1014 Args:
1015 script_text: The text of the presubmit script.
1016 presubmit_path: Project script to run.
1017 project: Project name to pass to presubmit script for bot selection.
1018
1019 Return:
1020 A map of try masters to map of builders to set of tests.
1021 """
1022 context = {}
1023 try:
1024 exec script_text in context
1025 except Exception, e:
1026 raise PresubmitFailure('"%s" had an exception.\n%s'
1027 % (presubmit_path, e))
1028
1029 function_name = 'GetPreferredTryMasters'
1030 if function_name not in context:
1031 return {}
1032 get_preferred_try_masters = context[function_name]
1033 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1034 raise PresubmitFailure(
1035 'Expected function "GetPreferredTryMasters" to take two arguments.')
1036 return get_preferred_try_masters(project, change)
1037
1038
rmistry@google.com5626a922015-02-26 14:03:30 +00001039class GetPostUploadExecuter(object):
1040 @staticmethod
1041 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1042 """Executes PostUploadHook() from a single presubmit script.
1043
1044 Args:
1045 script_text: The text of the presubmit script.
1046 presubmit_path: Project script to run.
1047 cl: The Changelist object.
1048 change: The Change object.
1049
1050 Return:
1051 A list of results objects.
1052 """
1053 context = {}
1054 try:
1055 exec script_text in context
1056 except Exception, e:
1057 raise PresubmitFailure('"%s" had an exception.\n%s'
1058 % (presubmit_path, e))
1059
1060 function_name = 'PostUploadHook'
1061 if function_name not in context:
1062 return {}
1063 post_upload_hook = context[function_name]
1064 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1065 raise PresubmitFailure(
1066 'Expected function "PostUploadHook" to take three arguments.')
1067 return post_upload_hook(cl, change, OutputApi(False))
1068
1069
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001070def _MergeMasters(masters1, masters2):
1071 """Merges two master maps. Merges also the tests of each builder."""
1072 result = {}
1073 for (master, builders) in itertools.chain(masters1.iteritems(),
1074 masters2.iteritems()):
1075 new_builders = result.setdefault(master, {})
1076 for (builder, tests) in builders.iteritems():
1077 new_builders.setdefault(builder, set([])).update(tests)
1078 return result
1079
1080
1081def DoGetTryMasters(change,
1082 changed_files,
1083 repository_root,
1084 default_presubmit,
1085 project,
1086 verbose,
1087 output_stream):
1088 """Get the list of try masters from the presubmit scripts.
1089
1090 Args:
1091 changed_files: List of modified files.
1092 repository_root: The repository root.
1093 default_presubmit: A default presubmit script to execute in any case.
1094 project: Optional name of a project used in selecting trybots.
1095 verbose: Prints debug info.
1096 output_stream: A stream to write debug output to.
1097
1098 Return:
1099 Map of try masters to map of builders to set of tests.
1100 """
1101 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1102 if not presubmit_files and verbose:
1103 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1104 results = {}
1105 executer = GetTryMastersExecuter()
1106
1107 if default_presubmit:
1108 if verbose:
1109 output_stream.write("Running default presubmit script.\n")
1110 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1111 results = _MergeMasters(results, executer.ExecPresubmitScript(
1112 default_presubmit, fake_path, project, change))
1113 for filename in presubmit_files:
1114 filename = os.path.abspath(filename)
1115 if verbose:
1116 output_stream.write("Running %s\n" % filename)
1117 # Accept CRLF presubmit script.
1118 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1119 results = _MergeMasters(results, executer.ExecPresubmitScript(
1120 presubmit_script, filename, project, change))
1121
1122 # Make sets to lists again for later JSON serialization.
1123 for builders in results.itervalues():
1124 for builder in builders:
1125 builders[builder] = list(builders[builder])
1126
1127 if results and verbose:
1128 output_stream.write('%s\n' % str(results))
1129 return results
1130
1131
rmistry@google.com5626a922015-02-26 14:03:30 +00001132def DoPostUploadExecuter(change,
1133 cl,
1134 repository_root,
1135 verbose,
1136 output_stream):
1137 """Execute the post upload hook.
1138
1139 Args:
1140 change: The Change object.
1141 cl: The Changelist object.
1142 repository_root: The repository root.
1143 verbose: Prints debug info.
1144 output_stream: A stream to write debug output to.
1145 """
1146 presubmit_files = ListRelevantPresubmitFiles(
1147 change.LocalPaths(), repository_root)
1148 if not presubmit_files and verbose:
1149 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1150 results = []
1151 executer = GetPostUploadExecuter()
1152 # The root presubmit file should be executed after the ones in subdirectories.
1153 # i.e. the specific post upload hooks should run before the general ones.
1154 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1155 presubmit_files.reverse()
1156
1157 for filename in presubmit_files:
1158 filename = os.path.abspath(filename)
1159 if verbose:
1160 output_stream.write("Running %s\n" % filename)
1161 # Accept CRLF presubmit script.
1162 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1163 results.extend(executer.ExecPresubmitScript(
1164 presubmit_script, filename, cl, change))
1165 output_stream.write('\n')
1166 if results:
1167 output_stream.write('** Post Upload Hook Messages **\n')
1168 for result in results:
1169 result.handle(output_stream)
1170 output_stream.write('\n')
1171
1172 return results
1173
1174
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001175class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001176 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001177 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001178 """
1179 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001180 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001181 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001182 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001183 gerrit_obj: provides basic Gerrit codereview functionality.
1184 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001185 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001186 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001187 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001188 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001189 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001190 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001191 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001192
1193 def ExecPresubmitScript(self, script_text, presubmit_path):
1194 """Executes a single presubmit script.
1195
1196 Args:
1197 script_text: The text of the presubmit script.
1198 presubmit_path: The path to the presubmit file (this will be reported via
1199 input_api.PresubmitLocalPath()).
1200
1201 Return:
1202 A list of result objects, empty if no problems.
1203 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001204
chase@chromium.org8e416c82009-10-06 04:30:44 +00001205 # Change to the presubmit file's directory to support local imports.
1206 main_path = os.getcwd()
1207 os.chdir(os.path.dirname(presubmit_path))
1208
1209 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001210 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001211 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001212 gerrit_obj=self.gerrit, dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001213 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001214 try:
1215 exec script_text in context
1216 except Exception, e:
1217 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001218
1219 # These function names must change if we make substantial changes to
1220 # the presubmit API that are not backwards compatible.
1221 if self.committing:
1222 function_name = 'CheckChangeOnCommit'
1223 else:
1224 function_name = 'CheckChangeOnUpload'
1225 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001226 context['__args'] = (input_api, OutputApi(self.committing))
tobiasjs2836bcf2016-08-16 04:08:16 -07001227 logging.debug('Running %s in %s', function_name, presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001228 result = eval(function_name + '(*__args)', context)
tobiasjs2836bcf2016-08-16 04:08:16 -07001229 logging.debug('Running %s done.', function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001230 if not (isinstance(result, types.TupleType) or
1231 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001232 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001233 'Presubmit functions must return a tuple or list')
1234 for item in result:
1235 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001236 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001237 'All presubmit results must be of types derived from '
1238 'output_api.PresubmitResult')
1239 else:
1240 result = () # no error since the script doesn't care about current event.
1241
scottmg86099d72016-09-01 09:16:51 -07001242 input_api.ShutdownPool()
1243
chase@chromium.org8e416c82009-10-06 04:30:44 +00001244 # Return the process to the original working directory.
1245 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001246 return result
1247
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001248def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001249 committing,
1250 verbose,
1251 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001252 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001253 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001254 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001255 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001256 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001257 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001258 """Runs all presubmit checks that apply to the files in the change.
1259
1260 This finds all PRESUBMIT.py files in directories enclosing the files in the
1261 change (up to the repository root) and calls the relevant entrypoint function
1262 depending on whether the change is being committed or uploaded.
1263
1264 Prints errors, warnings and notifications. Prompts the user for warnings
1265 when needed.
1266
1267 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001268 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001269 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001270 verbose: Prints debug info.
1271 output_stream: A stream to write output from presubmit tests to.
1272 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001273 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001274 may_prompt: Enable (y/n) questions on warning or error. If False,
1275 any questions are answered with yes by default.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001276 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001277 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001278 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001279
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001280 Warning:
1281 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1282 SHOULD be sys.stdin.
1283
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001284 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001285 A PresubmitOutput object. Use output.should_continue() to figure out
1286 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001287 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001288 old_environ = os.environ
1289 try:
1290 # Make sure python subprocesses won't generate .pyc files.
1291 os.environ = os.environ.copy()
1292 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001293
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001294 output = PresubmitOutput(input_stream, output_stream)
1295 if committing:
1296 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001297 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001298 output.write("Running presubmit upload checks ...\n")
1299 start_time = time.time()
1300 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001301 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001302 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001303 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001304 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001305 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001306 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001307 if default_presubmit:
1308 if verbose:
1309 output.write("Running default presubmit script.\n")
1310 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1311 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1312 for filename in presubmit_files:
1313 filename = os.path.abspath(filename)
1314 if verbose:
1315 output.write("Running %s\n" % filename)
1316 # Accept CRLF presubmit script.
1317 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1318 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001319
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001320 errors = []
1321 notifications = []
1322 warnings = []
1323 for result in results:
1324 if result.fatal:
1325 errors.append(result)
1326 elif result.should_prompt:
1327 warnings.append(result)
1328 else:
1329 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001330
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001331 output.write('\n')
1332 for name, items in (('Messages', notifications),
1333 ('Warnings', warnings),
1334 ('ERRORS', errors)):
1335 if items:
1336 output.write('** Presubmit %s **\n' % name)
1337 for item in items:
1338 item.handle(output)
1339 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001340
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001341 total_time = time.time() - start_time
1342 if total_time > 1.0:
1343 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001344
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001345 if errors:
1346 output.fail()
1347 elif warnings:
1348 output.write('There were presubmit warnings. ')
1349 if may_prompt:
1350 output.prompt_yes_no('Are you sure you wish to continue? (y/N): ')
1351 else:
1352 output.write('Presubmit checks passed.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001353
1354 global _ASKED_FOR_FEEDBACK
1355 # Ask for feedback one time out of 5.
1356 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001357 output.write(
1358 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1359 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1360 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001361 _ASKED_FOR_FEEDBACK = True
1362 return output
1363 finally:
1364 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001365
1366
1367def ScanSubDirs(mask, recursive):
1368 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001369 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001370
1371 results = []
1372 for root, dirs, files in os.walk('.'):
1373 if '.svn' in dirs:
1374 dirs.remove('.svn')
1375 if '.git' in dirs:
1376 dirs.remove('.git')
1377 for name in files:
1378 if fnmatch.fnmatch(name, mask):
1379 results.append(os.path.join(root, name))
1380 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001381
1382
1383def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001384 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001385 files = []
1386 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001387 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001388 return files
1389
1390
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001391def load_files(options, args):
1392 """Tries to determine the SCM."""
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001393 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001394 if args:
1395 files = ParseFiles(args, options.recursive)
agable0b65e732016-11-22 09:25:46 -08001396 change_scm = scm.determine_scm(options.root)
1397 if change_scm == 'git':
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001398 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001399 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001400 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001401 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001402 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001403 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001404 if not files:
1405 return None, None
1406 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001407 return change_class, files
1408
1409
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001410class NonexistantCannedCheckFilter(Exception):
1411 pass
1412
1413
1414@contextlib.contextmanager
1415def canned_check_filter(method_names):
1416 filtered = {}
1417 try:
1418 for method_name in method_names:
1419 if not hasattr(presubmit_canned_checks, method_name):
1420 raise NonexistantCannedCheckFilter(method_name)
1421 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1422 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1423 yield
1424 finally:
1425 for name, method in filtered.iteritems():
1426 setattr(presubmit_canned_checks, name, method)
1427
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001428
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001429def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001430 """Runs an external program, potentially from a child process created by the
1431 multiprocessing module.
1432
1433 multiprocessing needs a top level function with a single argument.
1434 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001435 cmd_data.kwargs['stdout'] = subprocess.PIPE
1436 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1437 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001438 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001439 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001440 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001441 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001442 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001443 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001444 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1445 if code != 0:
1446 return cmd_data.message(
1447 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1448 if cmd_data.info:
1449 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001450
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001451
sbc@chromium.org013731e2015-02-26 18:28:43 +00001452def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001453 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001454 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001455 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001456 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001457 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1458 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001459 parser.add_option("-r", "--recursive", action="store_true",
1460 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001461 parser.add_option("-v", "--verbose", action="count", default=0,
1462 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001463 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001464 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001465 parser.add_option("--description", default='')
1466 parser.add_option("--issue", type='int', default=0)
1467 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001468 parser.add_option("--root", default=os.getcwd(),
1469 help="Search for PRESUBMIT.py up to this directory. "
1470 "If inherit-review-settings-ok is present in this "
1471 "directory, parent directories up to the root file "
1472 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001473 parser.add_option("--upstream",
1474 help="Git only: the base ref or upstream branch against "
1475 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001476 parser.add_option("--default_presubmit")
1477 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001478 parser.add_option("--skip_canned", action='append', default=[],
1479 help="A list of checks to skip which appear in "
1480 "presubmit_canned_checks. Can be provided multiple times "
1481 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001482 parser.add_option("--dry_run", action='store_true',
1483 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001484 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001485 parser.add_option("--gerrit_fetch", action='store_true',
1486 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001487 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1488 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001489 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1490 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001491 # These are for OAuth2 authentication for bots. See also apply_issue.py
1492 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1493 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1494
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001495 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001496 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001497 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001498
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001499 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001500 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001501 elif options.verbose:
1502 logging.basicConfig(level=logging.INFO)
1503 else:
1504 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001505
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001506 if (any((options.rietveld_url, options.rietveld_email_file,
1507 options.rietveld_fetch, options.rietveld_private_key_file))
1508 and any((options.gerrit_url, options.gerrit_fetch))):
1509 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1510 'allowed')
1511
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001512 if options.rietveld_email and options.rietveld_email_file:
1513 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1514 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001515 if options.rietveld_email_file:
1516 with open(options.rietveld_email_file, "rb") as f:
1517 options.rietveld_email = f.read().strip()
1518
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001519 change_class, files = load_files(options, args)
1520 if not change_class:
1521 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001522 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001523
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001524 rietveld_obj, gerrit_obj = None, None
1525
maruel@chromium.org239f4112011-06-03 20:08:23 +00001526 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001527 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001528 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001529 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1530 options.rietveld_url,
1531 options.rietveld_email,
1532 options.rietveld_private_key_file)
1533 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001534 rietveld_obj = rietveld.CachingRietveld(
1535 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001536 auth_config,
1537 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001538 if options.rietveld_fetch:
1539 assert options.issue
1540 props = rietveld_obj.get_issue_properties(options.issue, False)
1541 options.author = props['owner_email']
1542 options.description = props['description']
1543 logging.info('Got author: "%s"', options.author)
1544 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001545
1546 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001547 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001548 rietveld_obj = None
1549 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1550 options.author = gerrit_obj.GetChangeOwner(options.issue)
1551 options.description = gerrit_obj.GetChangeDescription(options.issue,
1552 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001553 logging.info('Got author: "%s"', options.author)
1554 logging.info('Got description: """\n%s\n"""', options.description)
1555
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001556 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001557 with canned_check_filter(options.skip_canned):
1558 results = DoPresubmitChecks(
1559 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001560 options.description,
1561 options.root,
1562 files,
1563 options.issue,
1564 options.patchset,
1565 options.author,
1566 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001567 options.commit,
1568 options.verbose,
1569 sys.stdout,
1570 sys.stdin,
1571 options.default_presubmit,
1572 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001573 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001574 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001575 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001576 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001577 except NonexistantCannedCheckFilter, e:
1578 print >> sys.stderr, (
1579 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1580 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001581 except PresubmitFailure, e:
1582 print >> sys.stderr, e
1583 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1584 print >> sys.stderr, 'If all fails, contact maruel@'
1585 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001586
1587
1588if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001589 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001590 try:
1591 sys.exit(main())
1592 except KeyboardInterrupt:
1593 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001594 sys.exit(2)