blob: b089c0d0a91a239eb91fe2d328b62a54bd578bca [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
Aaron Gableb584c4f2017-04-26 16:28:08 -070046import git_footers
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000047import gerrit_util
dpranke@chromium.org2a009622011-03-01 02:43:31 +000048import owners
Jochen Eisinger76f5fc62017-04-07 16:27:46 +020049import owners_finder
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000051import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000052import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000053import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000054
55
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000056# Ask for feedback only once in program lifetime.
57_ASKED_FOR_FEEDBACK = False
58
59
maruel@chromium.org899e1c12011-04-07 17:03:18 +000060class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000061 pass
62
63
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000064class CommandData(object):
65 def __init__(self, name, cmd, kwargs, message):
66 self.name = name
67 self.cmd = cmd
68 self.kwargs = kwargs
69 self.message = message
70 self.info = None
71
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000072
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000073def normpath(path):
74 '''Version of os.path.normpath that also changes backward slashes to
75 forward slashes when not running on Windows.
76 '''
77 # This is safe to always do because the Windows version of os.path.normpath
78 # will replace forward slashes with backward slashes.
79 path = path.replace(os.sep, '/')
80 return os.path.normpath(path)
81
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000082
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000083def _RightHandSideLinesImpl(affected_files):
84 """Implements RightHandSideLines for InputApi and GclChange."""
85 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000086 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000087 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000088 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000089
90
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000091class PresubmitOutput(object):
92 def __init__(self, input_stream=None, output_stream=None):
93 self.input_stream = input_stream
94 self.output_stream = output_stream
95 self.reviewers = []
96 self.written_output = []
97 self.error_count = 0
98
99 def prompt_yes_no(self, prompt_string):
100 self.write(prompt_string)
101 if self.input_stream:
102 response = self.input_stream.readline().strip().lower()
103 if response not in ('y', 'yes'):
104 self.fail()
105 else:
106 self.fail()
107
108 def fail(self):
109 self.error_count += 1
110
111 def should_continue(self):
112 return not self.error_count
113
114 def write(self, s):
115 self.written_output.append(s)
116 if self.output_stream:
117 self.output_stream.write(s)
118
119 def getvalue(self):
120 return ''.join(self.written_output)
121
122
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000123# Top level object so multiprocessing can pickle
124# Public access through OutputApi object.
125class _PresubmitResult(object):
126 """Base class for result objects."""
127 fatal = False
128 should_prompt = False
129
130 def __init__(self, message, items=None, long_text=''):
131 """
132 message: A short one-line message to indicate errors.
133 items: A list of short strings to indicate where errors occurred.
134 long_text: multi-line text output, e.g. from another tool
135 """
136 self._message = message
137 self._items = items or []
138 if items:
139 self._items = items
140 self._long_text = long_text.rstrip()
141
142 def handle(self, output):
143 output.write(self._message)
144 output.write('\n')
145 for index, item in enumerate(self._items):
146 output.write(' ')
147 # Write separately in case it's unicode.
148 output.write(str(item))
149 if index < len(self._items) - 1:
150 output.write(' \\')
151 output.write('\n')
152 if self._long_text:
153 output.write('\n***************\n')
154 # Write separately in case it's unicode.
155 output.write(self._long_text)
156 output.write('\n***************\n')
157 if self.fatal:
158 output.fail()
159
160
161# Top level object so multiprocessing can pickle
162# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000163class _PresubmitError(_PresubmitResult):
164 """A hard presubmit error."""
165 fatal = True
166
167
168# Top level object so multiprocessing can pickle
169# Public access through OutputApi object.
170class _PresubmitPromptWarning(_PresubmitResult):
171 """An warning that prompts the user if they want to continue."""
172 should_prompt = True
173
174
175# Top level object so multiprocessing can pickle
176# Public access through OutputApi object.
177class _PresubmitNotifyResult(_PresubmitResult):
178 """Just print something to the screen -- but it's not even a warning."""
179 pass
180
181
182# Top level object so multiprocessing can pickle
183# Public access through OutputApi object.
184class _MailTextResult(_PresubmitResult):
185 """A warning that should be included in the review request email."""
186 def __init__(self, *args, **kwargs):
187 super(_MailTextResult, self).__init__()
188 raise NotImplementedError()
189
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000190class GerritAccessor(object):
191 """Limited Gerrit functionality for canned presubmit checks to work.
192
193 To avoid excessive Gerrit calls, caches the results.
194 """
195
196 def __init__(self, host):
197 self.host = host
198 self.cache = {}
199
200 def _FetchChangeDetail(self, issue):
201 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100202 try:
203 return gerrit_util.GetChangeDetail(
204 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700205 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100206 except gerrit_util.GerritError as e:
207 if e.http_status == 404:
208 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
209 'no credentials to fetch issue details' % issue)
210 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000211
212 def GetChangeInfo(self, issue):
213 """Returns labels and all revisions (patchsets) for this issue.
214
215 The result is a dictionary according to Gerrit REST Api.
216 https://gerrit-review.googlesource.com/Documentation/rest-api.html
217
218 However, API isn't very clear what's inside, so see tests for example.
219 """
220 assert issue
221 cache_key = int(issue)
222 if cache_key not in self.cache:
223 self.cache[cache_key] = self._FetchChangeDetail(issue)
224 return self.cache[cache_key]
225
226 def GetChangeDescription(self, issue, patchset=None):
227 """If patchset is none, fetches current patchset."""
228 info = self.GetChangeInfo(issue)
229 # info is a reference to cache. We'll modify it here adding description to
230 # it to the right patchset, if it is not yet there.
231
232 # Find revision info for the patchset we want.
233 if patchset is not None:
234 for rev, rev_info in info['revisions'].iteritems():
235 if str(rev_info['_number']) == str(patchset):
236 break
237 else:
238 raise Exception('patchset %s doesn\'t exist in issue %s' % (
239 patchset, issue))
240 else:
241 rev = info['current_revision']
242 rev_info = info['revisions'][rev]
243
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100244 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000245
246 def GetChangeOwner(self, issue):
247 return self.GetChangeInfo(issue)['owner']['email']
248
249 def GetChangeReviewers(self, issue, approving_only=True):
agable565adb52016-07-22 14:48:07 -0700250 cr = self.GetChangeInfo(issue)['labels']['Code-Review']
251 max_value = max(int(k) for k in cr['values'].keys())
Aaron Gablef5644a92016-12-02 15:31:58 -0800252 return [r.get('email') for r in cr.get('all', [])
agable565adb52016-07-22 14:48:07 -0700253 if not approving_only or r.get('value', 0) == max_value]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000254
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000255
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000257 """An instance of OutputApi gets passed to presubmit scripts so that they
258 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000259 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000260 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000261 PresubmitError = _PresubmitError
262 PresubmitPromptWarning = _PresubmitPromptWarning
263 PresubmitNotifyResult = _PresubmitNotifyResult
264 MailTextResult = _MailTextResult
265
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000266 def __init__(self, is_committing):
267 self.is_committing = is_committing
268
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000269 def PresubmitPromptOrNotify(self, *args, **kwargs):
270 """Warn the user when uploading, but only notify if committing."""
271 if self.is_committing:
272 return self.PresubmitNotifyResult(*args, **kwargs)
273 return self.PresubmitPromptWarning(*args, **kwargs)
274
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800275 def EnsureCQIncludeTrybotsAreAdded(self, cl, bots_to_include, message):
276 """Helper for any PostUploadHook wishing to add CQ_INCLUDE_TRYBOTS.
277
278 Merges the bots_to_include into the current CQ_INCLUDE_TRYBOTS list,
279 keeping it alphabetically sorted. Returns the results that should be
280 returned from the PostUploadHook.
281
282 Args:
283 cl: The git_cl.Changelist object.
284 bots_to_include: A list of strings of bots to include, in the form
285 "master:slave".
286 message: A message to be printed in the case that
287 CQ_INCLUDE_TRYBOTS was updated.
288 """
289 description = cl.GetDescription(force=True)
Aaron Gableb584c4f2017-04-26 16:28:08 -0700290 include_re = re.compile(r'^CQ_INCLUDE_TRYBOTS=(.*)$', re.M | re.I)
291
292 prior_bots = []
293 if cl.IsGerrit():
294 trybot_footers = git_footers.parse_footers(description).get(
295 git_footers.normalize_name('Cq-Include-Trybots'), [])
296 for f in trybot_footers:
297 prior_bots += [b.strip() for b in f.split(';') if b.strip()]
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800298 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700299 trybot_tags = include_re.finditer(description)
300 for t in trybot_tags:
301 prior_bots += [b.strip() for b in t.group(1).split(';') if b.strip()]
302
303 if set(prior_bots) >= set(bots_to_include):
304 return []
305 all_bots = ';'.join(sorted(set(prior_bots) | set(bots_to_include)))
306
307 if cl.IsGerrit():
308 description = git_footers.remove_footer(
309 description, 'Cq-Include-Trybots')
310 description = git_footers.add_footer(
311 description, 'Cq-Include-Trybots', all_bots,
312 before_keys=['Change-Id'])
313 else:
314 new_include_trybots = 'CQ_INCLUDE_TRYBOTS=%s' % all_bots
315 m = include_re.search(description)
316 if m:
317 description = include_re.sub(new_include_trybots, description)
Kenneth Russelldf6e7342017-04-24 17:07:41 -0700318 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700319 description = '%s\n%s\n' % (description, new_include_trybots)
320
321 cl.UpdateDescription(description, force=True)
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800322 return [self.PresubmitNotifyResult(message)]
323
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000324
325class InputApi(object):
326 """An instance of this object is passed to presubmit scripts so they can
327 know stuff about the change they're looking at.
328 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000329 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800330 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000331
maruel@chromium.org3410d912009-06-09 20:56:16 +0000332 # File extensions that are considered source files from a style guide
333 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000334 #
335 # Files without an extension aren't included in the list. If you want to
336 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
337 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000338 DEFAULT_WHITE_LIST = (
339 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000340 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
341 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000342 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000343 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000344 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000345 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000346 )
347
348 # Path regexp that should be excluded from being considered containing source
349 # files. Don't modify this list from a presubmit script!
350 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000351 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000352 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000353 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
354 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000355 # Output directories (just in case)
356 r".*\bDebug[\\\/].*",
357 r".*\bRelease[\\\/].*",
358 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000359 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000360 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000361 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000362 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000363 r"(|.*[\\\/])\.git[\\\/].*",
364 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000365 # There is no point in processing a patch file.
366 r".+\.diff$",
367 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000368 )
369
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000370 def __init__(self, change, presubmit_path, is_committing,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000371 rietveld_obj, verbose, gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000372 """Builds an InputApi object.
373
374 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000375 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000376 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000377 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000378 rietveld_obj: rietveld.Rietveld client object
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000379 gerrit_obj: provides basic Gerrit codereview functionality.
380 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000381 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000382 # Version number of the presubmit_support script.
383 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000384 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000385 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000386 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000387 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000388 self.dry_run = dry_run
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000389 # TBD
390 self.host_url = 'http://codereview.chromium.org'
391 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000392 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000393
394 # We expose various modules and functions as attributes of the input_api
395 # so that presubmit scripts don't have to import them.
396 self.basename = os.path.basename
397 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000398 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000399 self.cStringIO = cStringIO
dcheng091b7db2016-06-16 01:27:51 -0700400 self.fnmatch = fnmatch
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000401 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000402 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000403 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000404 self.os_listdir = os.listdir
405 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000406 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000407 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000408 self.pickle = pickle
409 self.marshal = marshal
410 self.re = re
411 self.subprocess = subprocess
412 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000413 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000414 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000415 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000416 self.urllib2 = urllib2
417
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000418 # To easily fork python.
419 self.python_executable = sys.executable
420 self.environ = os.environ
421
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000422 # InputApi.platform is the platform you're currently running on.
423 self.platform = sys.platform
424
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000425 self.cpu_count = multiprocessing.cpu_count()
426
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000427 # this is done here because in RunTests, the current working directory has
428 # changed, which causes Pool() to explode fantastically when run on windows
429 # (because it tries to load the __main__ module, which imports lots of
430 # things relative to the current working directory).
431 self._run_tests_pool = multiprocessing.Pool(self.cpu_count)
432
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000433 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000434 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000435
436 # We carry the canned checks so presubmit scripts can easily use them.
437 self.canned_checks = presubmit_canned_checks
438
Jochen Eisinger72606f82017-04-04 10:44:18 +0200439
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000440 # TODO(dpranke): figure out a list of all approved owners for a repo
441 # in order to be able to handle wildcard OWNERS files?
442 self.owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +0200443 fopen=file, os_path=self.os_path)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200444 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000445 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000446 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000447
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000448 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000449 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000450 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800451 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000452 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000453 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000454 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
455 for (a, b, header) in cpplint._re_pattern_templates
456 ]
457
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000458 def PresubmitLocalPath(self):
459 """Returns the local path of the presubmit script currently being run.
460
461 This is useful if you don't want to hard-code absolute paths in the
462 presubmit script. For example, It can be used to find another file
463 relative to the PRESUBMIT.py script, so the whole tree can be branched and
464 the presubmit script still works, without editing its content.
465 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000466 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000467
agable0b65e732016-11-22 09:25:46 -0800468 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000469 """Same as input_api.change.AffectedFiles() except only lists files
470 (and optionally directories) in the same directory as the current presubmit
471 script, or subdirectories thereof.
472 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000473 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000474 if len(dir_with_slash) == 1:
475 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000476
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000477 return filter(
478 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
agable0b65e732016-11-22 09:25:46 -0800479 self.change.AffectedFiles(include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000480
agable0b65e732016-11-22 09:25:46 -0800481 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000482 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800483 paths = [af.LocalPath() for af in self.AffectedFiles()]
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000484 logging.debug("LocalPaths: %s", paths)
485 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000486
agable0b65e732016-11-22 09:25:46 -0800487 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000488 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800489 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000490
agable0b65e732016-11-22 09:25:46 -0800491 def AffectedTestableFiles(self, include_deletes=None):
492 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000493 in the same directory as the current presubmit script, or subdirectories
494 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000495 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000496 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800497 warn("AffectedTestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000498 " is deprecated and ignored" % str(include_deletes),
499 category=DeprecationWarning,
500 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800501 return filter(lambda x: x.IsTestableFile(),
502 self.AffectedFiles(include_deletes=False))
503
504 def AffectedTextFiles(self, include_deletes=None):
505 """An alias to AffectedTestableFiles for backwards compatibility."""
506 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000507
maruel@chromium.org3410d912009-06-09 20:56:16 +0000508 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
509 """Filters out files that aren't considered "source file".
510
511 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
512 and InputApi.DEFAULT_BLACK_LIST is used respectively.
513
514 The lists will be compiled as regular expression and
515 AffectedFile.LocalPath() needs to pass both list.
516
517 Note: Copy-paste this function to suit your needs or use a lambda function.
518 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000519 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000520 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000521 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000522 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000523 return True
524 return False
525 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
526 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
527
528 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800529 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000530
531 If source_file is None, InputApi.FilterSourceFile() is used.
532 """
533 if not source_file:
534 source_file = self.FilterSourceFile
agable0b65e732016-11-22 09:25:46 -0800535 return filter(source_file, self.AffectedTestableFiles())
maruel@chromium.org3410d912009-06-09 20:56:16 +0000536
537 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000538 """An iterator over all text lines in "new" version of changed files.
539
540 Only lists lines from new or modified text files in the change that are
541 contained by the directory of the currently executing presubmit script.
542
543 This is useful for doing line-by-line regex checks, like checking for
544 trailing whitespace.
545
546 Yields:
547 a 3 tuple:
548 the AffectedFile instance of the current file;
549 integer line number (1-based); and
550 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000551
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000552 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000553 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000554 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000555 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000556
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000557 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000558 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000559
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000560 Deny reading anything outside the repository.
561 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000562 if isinstance(file_item, AffectedFile):
563 file_item = file_item.AbsoluteLocalPath()
564 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000565 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000566 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000567
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000568 @property
569 def tbr(self):
570 """Returns if a change is TBR'ed."""
571 return 'TBR' in self.change.tags
572
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000573 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000574 tests = []
575 msgs = []
576 for t in tests_mix:
577 if isinstance(t, OutputApi.PresubmitResult):
578 msgs.append(t)
579 else:
580 assert issubclass(t.message, _PresubmitResult)
581 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000582 if self.verbose:
583 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000584 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000585 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000586 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000587 else:
588 msgs.extend(map(CallCommand, tests))
589 return [m for m in msgs if m]
590
scottmg86099d72016-09-01 09:16:51 -0700591 def ShutdownPool(self):
592 self._run_tests_pool.close()
593 self._run_tests_pool.join()
594 self._run_tests_pool = None
595
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000596
nick@chromium.orgff526192013-06-10 19:30:26 +0000597class _DiffCache(object):
598 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000599 def __init__(self, upstream=None):
600 """Stores the upstream revision against which all diffs will be computed."""
601 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000602
603 def GetDiff(self, path, local_root):
604 """Get the diff for a particular path."""
605 raise NotImplementedError()
606
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700607 def GetOldContents(self, path, local_root):
608 """Get the old version for a particular path."""
609 raise NotImplementedError()
610
nick@chromium.orgff526192013-06-10 19:30:26 +0000611
nick@chromium.orgff526192013-06-10 19:30:26 +0000612class _GitDiffCache(_DiffCache):
613 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000614 def __init__(self, upstream):
615 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000616 self._diffs_by_file = None
617
618 def GetDiff(self, path, local_root):
619 if not self._diffs_by_file:
620 # Compute a single diff for all files and parse the output; should
621 # with git this is much faster than computing one diff for each file.
622 diffs = {}
623
624 # Don't specify any filenames below, because there are command line length
625 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000626 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
627 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000628
629 # This regex matches the path twice, separated by a space. Note that
630 # filename itself may contain spaces.
631 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
632 current_diff = []
633 keep_line_endings = True
634 for x in unified_diff.splitlines(keep_line_endings):
635 match = file_marker.match(x)
636 if match:
637 # Marks the start of a new per-file section.
638 diffs[match.group('filename')] = current_diff = [x]
639 elif x.startswith('diff --git'):
640 raise PresubmitFailure('Unexpected diff line: %s' % x)
641 else:
642 current_diff.append(x)
643
644 self._diffs_by_file = dict(
645 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
646
647 if path not in self._diffs_by_file:
648 raise PresubmitFailure(
649 'Unified diff did not contain entry for file %s' % path)
650
651 return self._diffs_by_file[path]
652
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700653 def GetOldContents(self, path, local_root):
654 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
655
nick@chromium.orgff526192013-06-10 19:30:26 +0000656
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000657class AffectedFile(object):
658 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000659
660 DIFF_CACHE = _DiffCache
661
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000662 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800663 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000664 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000665 self._path = path
666 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000667 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000668 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000669 self._cached_changed_contents = None
670 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000671 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700672 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000673
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000674 def LocalPath(self):
675 """Returns the path of this file on the local disk relative to client root.
676 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000677 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000678
679 def AbsoluteLocalPath(self):
680 """Returns the absolute path of this file on the local disk.
681 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000682 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684 def Action(self):
685 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000686 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687
agable0b65e732016-11-22 09:25:46 -0800688 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000689 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000690
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000691 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000692 raise NotImplementedError() # Implement when needed
693
agable0b65e732016-11-22 09:25:46 -0800694 def IsTextFile(self):
695 """An alias to IsTestableFile for backwards compatibility."""
696 return self.IsTestableFile()
697
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700698 def OldContents(self):
699 """Returns an iterator over the lines in the old version of file.
700
Daniel Cheng2da34fe2017-03-21 20:42:12 -0700701 The old version is the file before any modifications in the user's
702 workspace, i.e. the "left hand side".
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700703
704 Contents will be empty if the file is a directory or does not exist.
705 Note: The carriage returns (LF or CR) are stripped off.
706 """
707 return self._diff_cache.GetOldContents(self.LocalPath(),
708 self._local_root).splitlines()
709
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000710 def NewContents(self):
711 """Returns an iterator over the lines in the new version of file.
712
713 The new version is the file in the user's workspace, i.e. the "right hand
714 side".
715
716 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000717 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000718 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000719 if self._cached_new_contents is None:
720 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800721 try:
722 self._cached_new_contents = gclient_utils.FileRead(
723 self.AbsoluteLocalPath(), 'rU').splitlines()
724 except IOError:
725 pass # File not found? That's fine; maybe it was deleted.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000726 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000727
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000728 def ChangedContents(self):
729 """Returns a list of tuples (line number, line text) of all new lines.
730
731 This relies on the scm diff output describing each changed code section
732 with a line of the form
733
734 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
735 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000736 if self._cached_changed_contents is not None:
737 return self._cached_changed_contents[:]
738 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000739 line_num = 0
740
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000741 for line in self.GenerateScmDiff().splitlines():
742 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
743 if m:
744 line_num = int(m.groups(1)[0])
745 continue
746 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000747 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000748 if not line.startswith('-'):
749 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000750 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000751
maruel@chromium.org5de13972009-06-10 18:16:06 +0000752 def __str__(self):
753 return self.LocalPath()
754
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000755 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000756 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000757
maruel@chromium.org58407af2011-04-12 23:15:57 +0000758
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000759class GitAffectedFile(AffectedFile):
760 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000761 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800762 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000763
nick@chromium.orgff526192013-06-10 19:30:26 +0000764 DIFF_CACHE = _GitDiffCache
765
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000766 def __init__(self, *args, **kwargs):
767 AffectedFile.__init__(self, *args, **kwargs)
768 self._server_path = None
agable0b65e732016-11-22 09:25:46 -0800769 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000770
agable0b65e732016-11-22 09:25:46 -0800771 def IsTestableFile(self):
772 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000773 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -0800774 # A deleted file is not testable.
775 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000776 else:
agable0b65e732016-11-22 09:25:46 -0800777 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
778 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000779
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000780
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000781class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000782 """Describe a change.
783
784 Used directly by the presubmit scripts to query the current change being
785 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000786
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000787 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000788 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000789 self.KEY: equivalent to tags['KEY']
790 """
791
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000792 _AFFECTED_FILES = AffectedFile
793
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000794 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000795 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000796 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000797 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000798
maruel@chromium.org58407af2011-04-12 23:15:57 +0000799 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000800 self, name, description, local_root, files, issue, patchset, author,
801 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000802 if files is None:
803 files = []
804 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000805 # Convert root into an absolute path.
806 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000807 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000808 self.issue = issue
809 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000810 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000811
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000812 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000813 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000814 self._description_without_tags = ''
815 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000816
maruel@chromium.orge085d812011-10-10 19:49:15 +0000817 assert all(
818 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
819
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000820 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000821 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000822 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
823 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000824 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000825
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000826 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000827 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000828 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000829
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000830 def DescriptionText(self):
831 """Returns the user-entered changelist description, minus tags.
832
833 Any line in the user-provided description starting with e.g. "FOO="
834 (whitespace permitted before and around) is considered a tag line. Such
835 lines are stripped out of the description this function returns.
836 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000837 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000838
839 def FullDescriptionText(self):
840 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000841 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000842
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000843 def SetDescriptionText(self, description):
844 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000845
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000846 Also updates the list of tags."""
847 self._full_description = description
848
849 # From the description text, build up a dictionary of key/value pairs
850 # plus the description minus all key/value or "tag" lines.
851 description_without_tags = []
852 self.tags = {}
853 for line in self._full_description.splitlines():
854 m = self.TAG_LINE_RE.match(line)
855 if m:
856 self.tags[m.group('key')] = m.group('value')
857 else:
858 description_without_tags.append(line)
859
860 # Change back to text and remove whitespace at end.
861 self._description_without_tags = (
862 '\n'.join(description_without_tags).rstrip())
863
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000864 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000865 """Returns the repository (checkout) root directory for this change,
866 as an absolute path.
867 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000868 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000869
870 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000871 """Return tags directly as attributes on the object."""
872 if not re.match(r"^[A-Z_]*$", attr):
873 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000874 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000875
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000876 def AllFiles(self, root=None):
877 """List all files under source control in the repo."""
878 raise NotImplementedError()
879
agable0b65e732016-11-22 09:25:46 -0800880 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000881 """Returns a list of AffectedFile instances for all files in the change.
882
883 Args:
884 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +0000885 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000886
887 Returns:
888 [AffectedFile(path, action), AffectedFile(path, action)]
889 """
agable0b65e732016-11-22 09:25:46 -0800890 affected = filter(file_filter, self._affected_files)
sail@chromium.org5538e022011-05-12 17:53:16 +0000891
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000892 if include_deletes:
893 return affected
Lei Zhang9611c4c2017-04-04 01:41:56 -0700894 return filter(lambda x: x.Action() != 'D', affected)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000895
agable0b65e732016-11-22 09:25:46 -0800896 def AffectedTestableFiles(self, include_deletes=None):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000897 """Return a list of the existing text files in a change."""
898 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800899 warn("AffectedTeestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000900 " is deprecated and ignored" % str(include_deletes),
901 category=DeprecationWarning,
902 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800903 return filter(lambda x: x.IsTestableFile(),
904 self.AffectedFiles(include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000905
agable0b65e732016-11-22 09:25:46 -0800906 def AffectedTextFiles(self, include_deletes=None):
907 """An alias to AffectedTestableFiles for backwards compatibility."""
908 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000909
agable0b65e732016-11-22 09:25:46 -0800910 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000911 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800912 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000913
agable0b65e732016-11-22 09:25:46 -0800914 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000915 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800916 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000917
918 def RightHandSideLines(self):
919 """An iterator over all text lines in "new" version of changed files.
920
921 Lists lines from new or modified text files in the change.
922
923 This is useful for doing line-by-line regex checks, like checking for
924 trailing whitespace.
925
926 Yields:
927 a 3 tuple:
928 the AffectedFile instance of the current file;
929 integer line number (1-based); and
930 the contents of the line as a string.
931 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000932 return _RightHandSideLinesImpl(
933 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -0800934 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000935
Jochen Eisingerd0573ec2017-04-13 10:55:06 +0200936 def OriginalOwnersFiles(self):
937 """A map from path names of affected OWNERS files to their old content."""
938 def owners_file_filter(f):
939 return 'OWNERS' in os.path.split(f.LocalPath())[1]
940 files = self.AffectedFiles(file_filter=owners_file_filter)
941 return dict([(f.LocalPath(), f.OldContents()) for f in files])
942
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000943
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000944class GitChange(Change):
945 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000946 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000947
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000948 def AllFiles(self, root=None):
949 """List all files under source control in the repo."""
950 root = root or self.RepositoryRoot()
951 return subprocess.check_output(
952 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
953
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000954
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000955def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000956 """Finds all presubmit files that apply to a given set of source files.
957
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000958 If inherit-review-settings-ok is present right under root, looks for
959 PRESUBMIT.py in directories enclosing root.
960
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000961 Args:
962 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000963 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000964
965 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000966 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000967 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000968 files = [normpath(os.path.join(root, f)) for f in files]
969
970 # List all the individual directories containing files.
971 directories = set([os.path.dirname(f) for f in files])
972
973 # Ignore root if inherit-review-settings-ok is present.
974 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
975 root = None
976
977 # Collect all unique directories that may contain PRESUBMIT.py.
978 candidates = set()
979 for directory in directories:
980 while True:
981 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000982 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000983 candidates.add(directory)
984 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000985 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000986 parent_dir = os.path.dirname(directory)
987 if parent_dir == directory:
988 # We hit the system root directory.
989 break
990 directory = parent_dir
991
992 # Look for PRESUBMIT.py in all candidate directories.
993 results = []
994 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -0700995 try:
996 for f in os.listdir(directory):
997 p = os.path.join(directory, f)
998 if os.path.isfile(p) and re.match(
999 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1000 results.append(p)
1001 except OSError:
1002 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001003
tobiasjs2836bcf2016-08-16 04:08:16 -07001004 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001005 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006
1007
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001008class GetTryMastersExecuter(object):
1009 @staticmethod
1010 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1011 """Executes GetPreferredTryMasters() from a single presubmit script.
1012
1013 Args:
1014 script_text: The text of the presubmit script.
1015 presubmit_path: Project script to run.
1016 project: Project name to pass to presubmit script for bot selection.
1017
1018 Return:
1019 A map of try masters to map of builders to set of tests.
1020 """
1021 context = {}
1022 try:
1023 exec script_text in context
1024 except Exception, e:
1025 raise PresubmitFailure('"%s" had an exception.\n%s'
1026 % (presubmit_path, e))
1027
1028 function_name = 'GetPreferredTryMasters'
1029 if function_name not in context:
1030 return {}
1031 get_preferred_try_masters = context[function_name]
1032 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1033 raise PresubmitFailure(
1034 'Expected function "GetPreferredTryMasters" to take two arguments.')
1035 return get_preferred_try_masters(project, change)
1036
1037
rmistry@google.com5626a922015-02-26 14:03:30 +00001038class GetPostUploadExecuter(object):
1039 @staticmethod
1040 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1041 """Executes PostUploadHook() from a single presubmit script.
1042
1043 Args:
1044 script_text: The text of the presubmit script.
1045 presubmit_path: Project script to run.
1046 cl: The Changelist object.
1047 change: The Change object.
1048
1049 Return:
1050 A list of results objects.
1051 """
1052 context = {}
1053 try:
1054 exec script_text in context
1055 except Exception, e:
1056 raise PresubmitFailure('"%s" had an exception.\n%s'
1057 % (presubmit_path, e))
1058
1059 function_name = 'PostUploadHook'
1060 if function_name not in context:
1061 return {}
1062 post_upload_hook = context[function_name]
1063 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1064 raise PresubmitFailure(
1065 'Expected function "PostUploadHook" to take three arguments.')
1066 return post_upload_hook(cl, change, OutputApi(False))
1067
1068
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001069def _MergeMasters(masters1, masters2):
1070 """Merges two master maps. Merges also the tests of each builder."""
1071 result = {}
1072 for (master, builders) in itertools.chain(masters1.iteritems(),
1073 masters2.iteritems()):
1074 new_builders = result.setdefault(master, {})
1075 for (builder, tests) in builders.iteritems():
1076 new_builders.setdefault(builder, set([])).update(tests)
1077 return result
1078
1079
1080def DoGetTryMasters(change,
1081 changed_files,
1082 repository_root,
1083 default_presubmit,
1084 project,
1085 verbose,
1086 output_stream):
1087 """Get the list of try masters from the presubmit scripts.
1088
1089 Args:
1090 changed_files: List of modified files.
1091 repository_root: The repository root.
1092 default_presubmit: A default presubmit script to execute in any case.
1093 project: Optional name of a project used in selecting trybots.
1094 verbose: Prints debug info.
1095 output_stream: A stream to write debug output to.
1096
1097 Return:
1098 Map of try masters to map of builders to set of tests.
1099 """
1100 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1101 if not presubmit_files and verbose:
1102 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1103 results = {}
1104 executer = GetTryMastersExecuter()
1105
1106 if default_presubmit:
1107 if verbose:
1108 output_stream.write("Running default presubmit script.\n")
1109 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1110 results = _MergeMasters(results, executer.ExecPresubmitScript(
1111 default_presubmit, fake_path, project, change))
1112 for filename in presubmit_files:
1113 filename = os.path.abspath(filename)
1114 if verbose:
1115 output_stream.write("Running %s\n" % filename)
1116 # Accept CRLF presubmit script.
1117 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1118 results = _MergeMasters(results, executer.ExecPresubmitScript(
1119 presubmit_script, filename, project, change))
1120
1121 # Make sets to lists again for later JSON serialization.
1122 for builders in results.itervalues():
1123 for builder in builders:
1124 builders[builder] = list(builders[builder])
1125
1126 if results and verbose:
1127 output_stream.write('%s\n' % str(results))
1128 return results
1129
1130
rmistry@google.com5626a922015-02-26 14:03:30 +00001131def DoPostUploadExecuter(change,
1132 cl,
1133 repository_root,
1134 verbose,
1135 output_stream):
1136 """Execute the post upload hook.
1137
1138 Args:
1139 change: The Change object.
1140 cl: The Changelist object.
1141 repository_root: The repository root.
1142 verbose: Prints debug info.
1143 output_stream: A stream to write debug output to.
1144 """
1145 presubmit_files = ListRelevantPresubmitFiles(
1146 change.LocalPaths(), repository_root)
1147 if not presubmit_files and verbose:
1148 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1149 results = []
1150 executer = GetPostUploadExecuter()
1151 # The root presubmit file should be executed after the ones in subdirectories.
1152 # i.e. the specific post upload hooks should run before the general ones.
1153 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1154 presubmit_files.reverse()
1155
1156 for filename in presubmit_files:
1157 filename = os.path.abspath(filename)
1158 if verbose:
1159 output_stream.write("Running %s\n" % filename)
1160 # Accept CRLF presubmit script.
1161 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1162 results.extend(executer.ExecPresubmitScript(
1163 presubmit_script, filename, cl, change))
1164 output_stream.write('\n')
1165 if results:
1166 output_stream.write('** Post Upload Hook Messages **\n')
1167 for result in results:
1168 result.handle(output_stream)
1169 output_stream.write('\n')
1170
1171 return results
1172
1173
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001174class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001175 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001176 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001177 """
1178 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001179 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001180 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001181 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001182 gerrit_obj: provides basic Gerrit codereview functionality.
1183 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001184 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001185 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001186 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001187 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001188 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001189 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001190 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001191
1192 def ExecPresubmitScript(self, script_text, presubmit_path):
1193 """Executes a single presubmit script.
1194
1195 Args:
1196 script_text: The text of the presubmit script.
1197 presubmit_path: The path to the presubmit file (this will be reported via
1198 input_api.PresubmitLocalPath()).
1199
1200 Return:
1201 A list of result objects, empty if no problems.
1202 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001203
chase@chromium.org8e416c82009-10-06 04:30:44 +00001204 # Change to the presubmit file's directory to support local imports.
1205 main_path = os.getcwd()
1206 os.chdir(os.path.dirname(presubmit_path))
1207
1208 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001209 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001210 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001211 gerrit_obj=self.gerrit, dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001212 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001213 try:
1214 exec script_text in context
1215 except Exception, e:
1216 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001217
1218 # These function names must change if we make substantial changes to
1219 # the presubmit API that are not backwards compatible.
1220 if self.committing:
1221 function_name = 'CheckChangeOnCommit'
1222 else:
1223 function_name = 'CheckChangeOnUpload'
1224 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001225 context['__args'] = (input_api, OutputApi(self.committing))
tobiasjs2836bcf2016-08-16 04:08:16 -07001226 logging.debug('Running %s in %s', function_name, presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001227 result = eval(function_name + '(*__args)', context)
tobiasjs2836bcf2016-08-16 04:08:16 -07001228 logging.debug('Running %s done.', function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001229 if not (isinstance(result, types.TupleType) or
1230 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001231 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001232 'Presubmit functions must return a tuple or list')
1233 for item in result:
1234 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001235 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001236 'All presubmit results must be of types derived from '
1237 'output_api.PresubmitResult')
1238 else:
1239 result = () # no error since the script doesn't care about current event.
1240
scottmg86099d72016-09-01 09:16:51 -07001241 input_api.ShutdownPool()
1242
chase@chromium.org8e416c82009-10-06 04:30:44 +00001243 # Return the process to the original working directory.
1244 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001245 return result
1246
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001247def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001248 committing,
1249 verbose,
1250 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001251 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001252 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001253 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001254 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001255 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001256 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001257 """Runs all presubmit checks that apply to the files in the change.
1258
1259 This finds all PRESUBMIT.py files in directories enclosing the files in the
1260 change (up to the repository root) and calls the relevant entrypoint function
1261 depending on whether the change is being committed or uploaded.
1262
1263 Prints errors, warnings and notifications. Prompts the user for warnings
1264 when needed.
1265
1266 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001267 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001268 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001269 verbose: Prints debug info.
1270 output_stream: A stream to write output from presubmit tests to.
1271 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001272 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001273 may_prompt: Enable (y/n) questions on warning or error. If False,
1274 any questions are answered with yes by default.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001275 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001276 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001277 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001278
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001279 Warning:
1280 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1281 SHOULD be sys.stdin.
1282
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001283 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001284 A PresubmitOutput object. Use output.should_continue() to figure out
1285 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001286 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001287 old_environ = os.environ
1288 try:
1289 # Make sure python subprocesses won't generate .pyc files.
1290 os.environ = os.environ.copy()
1291 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001292
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001293 output = PresubmitOutput(input_stream, output_stream)
1294 if committing:
1295 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001296 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001297 output.write("Running presubmit upload checks ...\n")
1298 start_time = time.time()
1299 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001300 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001301 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001302 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001303 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001304 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001305 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001306 if default_presubmit:
1307 if verbose:
1308 output.write("Running default presubmit script.\n")
1309 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1310 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1311 for filename in presubmit_files:
1312 filename = os.path.abspath(filename)
1313 if verbose:
1314 output.write("Running %s\n" % filename)
1315 # Accept CRLF presubmit script.
1316 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1317 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001318
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001319 errors = []
1320 notifications = []
1321 warnings = []
1322 for result in results:
1323 if result.fatal:
1324 errors.append(result)
1325 elif result.should_prompt:
1326 warnings.append(result)
1327 else:
1328 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001329
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001330 output.write('\n')
1331 for name, items in (('Messages', notifications),
1332 ('Warnings', warnings),
1333 ('ERRORS', errors)):
1334 if items:
1335 output.write('** Presubmit %s **\n' % name)
1336 for item in items:
1337 item.handle(output)
1338 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001339
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001340 total_time = time.time() - start_time
1341 if total_time > 1.0:
1342 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001343
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001344 if errors:
1345 output.fail()
1346 elif warnings:
1347 output.write('There were presubmit warnings. ')
1348 if may_prompt:
1349 output.prompt_yes_no('Are you sure you wish to continue? (y/N): ')
1350 else:
1351 output.write('Presubmit checks passed.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001352
1353 global _ASKED_FOR_FEEDBACK
1354 # Ask for feedback one time out of 5.
1355 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001356 output.write(
1357 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1358 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1359 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001360 _ASKED_FOR_FEEDBACK = True
1361 return output
1362 finally:
1363 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001364
1365
1366def ScanSubDirs(mask, recursive):
1367 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001368 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001369
1370 results = []
1371 for root, dirs, files in os.walk('.'):
1372 if '.svn' in dirs:
1373 dirs.remove('.svn')
1374 if '.git' in dirs:
1375 dirs.remove('.git')
1376 for name in files:
1377 if fnmatch.fnmatch(name, mask):
1378 results.append(os.path.join(root, name))
1379 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001380
1381
1382def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001383 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001384 files = []
1385 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001386 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001387 return files
1388
1389
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001390def load_files(options, args):
1391 """Tries to determine the SCM."""
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001392 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001393 if args:
1394 files = ParseFiles(args, options.recursive)
agable0b65e732016-11-22 09:25:46 -08001395 change_scm = scm.determine_scm(options.root)
1396 if change_scm == 'git':
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001397 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001398 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001399 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001400 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001401 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001402 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001403 if not files:
1404 return None, None
1405 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001406 return change_class, files
1407
1408
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001409class NonexistantCannedCheckFilter(Exception):
1410 pass
1411
1412
1413@contextlib.contextmanager
1414def canned_check_filter(method_names):
1415 filtered = {}
1416 try:
1417 for method_name in method_names:
1418 if not hasattr(presubmit_canned_checks, method_name):
1419 raise NonexistantCannedCheckFilter(method_name)
1420 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1421 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1422 yield
1423 finally:
1424 for name, method in filtered.iteritems():
1425 setattr(presubmit_canned_checks, name, method)
1426
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001427
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001428def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001429 """Runs an external program, potentially from a child process created by the
1430 multiprocessing module.
1431
1432 multiprocessing needs a top level function with a single argument.
1433 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001434 cmd_data.kwargs['stdout'] = subprocess.PIPE
1435 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1436 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001437 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001438 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001439 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001440 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001441 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001442 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001443 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1444 if code != 0:
1445 return cmd_data.message(
1446 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1447 if cmd_data.info:
1448 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001449
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001450
sbc@chromium.org013731e2015-02-26 18:28:43 +00001451def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001452 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001453 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001454 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001455 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001456 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1457 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001458 parser.add_option("-r", "--recursive", action="store_true",
1459 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001460 parser.add_option("-v", "--verbose", action="count", default=0,
1461 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001462 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001463 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001464 parser.add_option("--description", default='')
1465 parser.add_option("--issue", type='int', default=0)
1466 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001467 parser.add_option("--root", default=os.getcwd(),
1468 help="Search for PRESUBMIT.py up to this directory. "
1469 "If inherit-review-settings-ok is present in this "
1470 "directory, parent directories up to the root file "
1471 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001472 parser.add_option("--upstream",
1473 help="Git only: the base ref or upstream branch against "
1474 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001475 parser.add_option("--default_presubmit")
1476 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001477 parser.add_option("--skip_canned", action='append', default=[],
1478 help="A list of checks to skip which appear in "
1479 "presubmit_canned_checks. Can be provided multiple times "
1480 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001481 parser.add_option("--dry_run", action='store_true',
1482 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001483 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001484 parser.add_option("--gerrit_fetch", action='store_true',
1485 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001486 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1487 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001488 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1489 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001490 # These are for OAuth2 authentication for bots. See also apply_issue.py
1491 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1492 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1493
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001494 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001495 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001496 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001497
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001498 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001499 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001500 elif options.verbose:
1501 logging.basicConfig(level=logging.INFO)
1502 else:
1503 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001504
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001505 if (any((options.rietveld_url, options.rietveld_email_file,
1506 options.rietveld_fetch, options.rietveld_private_key_file))
1507 and any((options.gerrit_url, options.gerrit_fetch))):
1508 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1509 'allowed')
1510
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001511 if options.rietveld_email and options.rietveld_email_file:
1512 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1513 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001514 if options.rietveld_email_file:
1515 with open(options.rietveld_email_file, "rb") as f:
1516 options.rietveld_email = f.read().strip()
1517
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001518 change_class, files = load_files(options, args)
1519 if not change_class:
1520 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001521 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001522
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001523 rietveld_obj, gerrit_obj = None, None
1524
maruel@chromium.org239f4112011-06-03 20:08:23 +00001525 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001526 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001527 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001528 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1529 options.rietveld_url,
1530 options.rietveld_email,
1531 options.rietveld_private_key_file)
1532 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001533 rietveld_obj = rietveld.CachingRietveld(
1534 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001535 auth_config,
1536 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001537 if options.rietveld_fetch:
1538 assert options.issue
1539 props = rietveld_obj.get_issue_properties(options.issue, False)
1540 options.author = props['owner_email']
1541 options.description = props['description']
1542 logging.info('Got author: "%s"', options.author)
1543 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001544
1545 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001546 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001547 rietveld_obj = None
1548 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1549 options.author = gerrit_obj.GetChangeOwner(options.issue)
1550 options.description = gerrit_obj.GetChangeDescription(options.issue,
1551 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001552 logging.info('Got author: "%s"', options.author)
1553 logging.info('Got description: """\n%s\n"""', options.description)
1554
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001555 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001556 with canned_check_filter(options.skip_canned):
1557 results = DoPresubmitChecks(
1558 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001559 options.description,
1560 options.root,
1561 files,
1562 options.issue,
1563 options.patchset,
1564 options.author,
1565 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001566 options.commit,
1567 options.verbose,
1568 sys.stdout,
1569 sys.stdin,
1570 options.default_presubmit,
1571 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001572 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001573 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001574 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001575 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001576 except NonexistantCannedCheckFilter, e:
1577 print >> sys.stderr, (
1578 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1579 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001580 except PresubmitFailure, e:
1581 print >> sys.stderr, e
1582 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1583 print >> sys.stderr, 'If all fails, contact maruel@'
1584 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001585
1586
1587if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001588 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001589 try:
1590 sys.exit(main())
1591 except KeyboardInterrupt:
1592 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001593 sys.exit(2)