blob: 81634c59231c1fb381c4a0ccea6cc43fae4de466 [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
Aaron Gablefc03e672017-05-15 14:09:42 -0700876 # TODO(agable): Update these to also get "Git-Footer: Foo"-style footers.
877 def BugsFromDescription(self):
878 """Returns all bugs referenced in the commit description."""
879 return [b.strip() for b in self.tags.get('BUG', '').split(',') if b.strip()]
880
881 def ReviewersFromDescription(self):
882 """Returns all reviewers listed in the commit description."""
883 return [r.strip() for r in self.tags.get('R', '').split(',') if r.strip()]
884
885 def TBRsFromDescription(self):
886 """Returns all TBR reviewers listed in the commit description."""
887 return [r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()]
888
889 # TODO(agable): Delete these once we're sure they're unused.
890 @property
891 def BUG(self):
892 return ','.join(self.BugsFromDescription())
893 @property
894 def R(self):
895 return ','.join(self.ReviewersFromDescription())
896 @property
897 def TBR(self):
898 return ','.join(self.TBRsFromDescription())
899
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000900 def AllFiles(self, root=None):
901 """List all files under source control in the repo."""
902 raise NotImplementedError()
903
agable0b65e732016-11-22 09:25:46 -0800904 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000905 """Returns a list of AffectedFile instances for all files in the change.
906
907 Args:
908 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +0000909 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000910
911 Returns:
912 [AffectedFile(path, action), AffectedFile(path, action)]
913 """
agable0b65e732016-11-22 09:25:46 -0800914 affected = filter(file_filter, self._affected_files)
sail@chromium.org5538e022011-05-12 17:53:16 +0000915
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000916 if include_deletes:
917 return affected
Lei Zhang9611c4c2017-04-04 01:41:56 -0700918 return filter(lambda x: x.Action() != 'D', affected)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000919
agable0b65e732016-11-22 09:25:46 -0800920 def AffectedTestableFiles(self, include_deletes=None):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000921 """Return a list of the existing text files in a change."""
922 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800923 warn("AffectedTeestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000924 " is deprecated and ignored" % str(include_deletes),
925 category=DeprecationWarning,
926 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800927 return filter(lambda x: x.IsTestableFile(),
928 self.AffectedFiles(include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000929
agable0b65e732016-11-22 09:25:46 -0800930 def AffectedTextFiles(self, include_deletes=None):
931 """An alias to AffectedTestableFiles for backwards compatibility."""
932 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000933
agable0b65e732016-11-22 09:25:46 -0800934 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000935 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800936 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000937
agable0b65e732016-11-22 09:25:46 -0800938 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000939 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -0800940 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000941
942 def RightHandSideLines(self):
943 """An iterator over all text lines in "new" version of changed files.
944
945 Lists lines from new or modified text files in the change.
946
947 This is useful for doing line-by-line regex checks, like checking for
948 trailing whitespace.
949
950 Yields:
951 a 3 tuple:
952 the AffectedFile instance of the current file;
953 integer line number (1-based); and
954 the contents of the line as a string.
955 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000956 return _RightHandSideLinesImpl(
957 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -0800958 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000959
Jochen Eisingerd0573ec2017-04-13 10:55:06 +0200960 def OriginalOwnersFiles(self):
961 """A map from path names of affected OWNERS files to their old content."""
962 def owners_file_filter(f):
963 return 'OWNERS' in os.path.split(f.LocalPath())[1]
964 files = self.AffectedFiles(file_filter=owners_file_filter)
965 return dict([(f.LocalPath(), f.OldContents()) for f in files])
966
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000967
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000968class GitChange(Change):
969 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000970 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000971
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000972 def AllFiles(self, root=None):
973 """List all files under source control in the repo."""
974 root = root or self.RepositoryRoot()
975 return subprocess.check_output(
976 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
977
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000978
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000979def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000980 """Finds all presubmit files that apply to a given set of source files.
981
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000982 If inherit-review-settings-ok is present right under root, looks for
983 PRESUBMIT.py in directories enclosing root.
984
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000985 Args:
986 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000987 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000988
989 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000990 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000991 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000992 files = [normpath(os.path.join(root, f)) for f in files]
993
994 # List all the individual directories containing files.
995 directories = set([os.path.dirname(f) for f in files])
996
997 # Ignore root if inherit-review-settings-ok is present.
998 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
999 root = None
1000
1001 # Collect all unique directories that may contain PRESUBMIT.py.
1002 candidates = set()
1003 for directory in directories:
1004 while True:
1005 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001007 candidates.add(directory)
1008 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001009 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001010 parent_dir = os.path.dirname(directory)
1011 if parent_dir == directory:
1012 # We hit the system root directory.
1013 break
1014 directory = parent_dir
1015
1016 # Look for PRESUBMIT.py in all candidate directories.
1017 results = []
1018 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -07001019 try:
1020 for f in os.listdir(directory):
1021 p = os.path.join(directory, f)
1022 if os.path.isfile(p) and re.match(
1023 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1024 results.append(p)
1025 except OSError:
1026 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001027
tobiasjs2836bcf2016-08-16 04:08:16 -07001028 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001029 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001030
1031
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001032class GetTryMastersExecuter(object):
1033 @staticmethod
1034 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1035 """Executes GetPreferredTryMasters() from a single presubmit script.
1036
1037 Args:
1038 script_text: The text of the presubmit script.
1039 presubmit_path: Project script to run.
1040 project: Project name to pass to presubmit script for bot selection.
1041
1042 Return:
1043 A map of try masters to map of builders to set of tests.
1044 """
1045 context = {}
1046 try:
1047 exec script_text in context
1048 except Exception, e:
1049 raise PresubmitFailure('"%s" had an exception.\n%s'
1050 % (presubmit_path, e))
1051
1052 function_name = 'GetPreferredTryMasters'
1053 if function_name not in context:
1054 return {}
1055 get_preferred_try_masters = context[function_name]
1056 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1057 raise PresubmitFailure(
1058 'Expected function "GetPreferredTryMasters" to take two arguments.')
1059 return get_preferred_try_masters(project, change)
1060
1061
rmistry@google.com5626a922015-02-26 14:03:30 +00001062class GetPostUploadExecuter(object):
1063 @staticmethod
1064 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1065 """Executes PostUploadHook() from a single presubmit script.
1066
1067 Args:
1068 script_text: The text of the presubmit script.
1069 presubmit_path: Project script to run.
1070 cl: The Changelist object.
1071 change: The Change object.
1072
1073 Return:
1074 A list of results objects.
1075 """
1076 context = {}
1077 try:
1078 exec script_text in context
1079 except Exception, e:
1080 raise PresubmitFailure('"%s" had an exception.\n%s'
1081 % (presubmit_path, e))
1082
1083 function_name = 'PostUploadHook'
1084 if function_name not in context:
1085 return {}
1086 post_upload_hook = context[function_name]
1087 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1088 raise PresubmitFailure(
1089 'Expected function "PostUploadHook" to take three arguments.')
1090 return post_upload_hook(cl, change, OutputApi(False))
1091
1092
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001093def _MergeMasters(masters1, masters2):
1094 """Merges two master maps. Merges also the tests of each builder."""
1095 result = {}
1096 for (master, builders) in itertools.chain(masters1.iteritems(),
1097 masters2.iteritems()):
1098 new_builders = result.setdefault(master, {})
1099 for (builder, tests) in builders.iteritems():
1100 new_builders.setdefault(builder, set([])).update(tests)
1101 return result
1102
1103
1104def DoGetTryMasters(change,
1105 changed_files,
1106 repository_root,
1107 default_presubmit,
1108 project,
1109 verbose,
1110 output_stream):
1111 """Get the list of try masters from the presubmit scripts.
1112
1113 Args:
1114 changed_files: List of modified files.
1115 repository_root: The repository root.
1116 default_presubmit: A default presubmit script to execute in any case.
1117 project: Optional name of a project used in selecting trybots.
1118 verbose: Prints debug info.
1119 output_stream: A stream to write debug output to.
1120
1121 Return:
1122 Map of try masters to map of builders to set of tests.
1123 """
1124 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1125 if not presubmit_files and verbose:
1126 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1127 results = {}
1128 executer = GetTryMastersExecuter()
1129
1130 if default_presubmit:
1131 if verbose:
1132 output_stream.write("Running default presubmit script.\n")
1133 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1134 results = _MergeMasters(results, executer.ExecPresubmitScript(
1135 default_presubmit, fake_path, project, change))
1136 for filename in presubmit_files:
1137 filename = os.path.abspath(filename)
1138 if verbose:
1139 output_stream.write("Running %s\n" % filename)
1140 # Accept CRLF presubmit script.
1141 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1142 results = _MergeMasters(results, executer.ExecPresubmitScript(
1143 presubmit_script, filename, project, change))
1144
1145 # Make sets to lists again for later JSON serialization.
1146 for builders in results.itervalues():
1147 for builder in builders:
1148 builders[builder] = list(builders[builder])
1149
1150 if results and verbose:
1151 output_stream.write('%s\n' % str(results))
1152 return results
1153
1154
rmistry@google.com5626a922015-02-26 14:03:30 +00001155def DoPostUploadExecuter(change,
1156 cl,
1157 repository_root,
1158 verbose,
1159 output_stream):
1160 """Execute the post upload hook.
1161
1162 Args:
1163 change: The Change object.
1164 cl: The Changelist object.
1165 repository_root: The repository root.
1166 verbose: Prints debug info.
1167 output_stream: A stream to write debug output to.
1168 """
1169 presubmit_files = ListRelevantPresubmitFiles(
1170 change.LocalPaths(), repository_root)
1171 if not presubmit_files and verbose:
1172 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1173 results = []
1174 executer = GetPostUploadExecuter()
1175 # The root presubmit file should be executed after the ones in subdirectories.
1176 # i.e. the specific post upload hooks should run before the general ones.
1177 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1178 presubmit_files.reverse()
1179
1180 for filename in presubmit_files:
1181 filename = os.path.abspath(filename)
1182 if verbose:
1183 output_stream.write("Running %s\n" % filename)
1184 # Accept CRLF presubmit script.
1185 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1186 results.extend(executer.ExecPresubmitScript(
1187 presubmit_script, filename, cl, change))
1188 output_stream.write('\n')
1189 if results:
1190 output_stream.write('** Post Upload Hook Messages **\n')
1191 for result in results:
1192 result.handle(output_stream)
1193 output_stream.write('\n')
1194
1195 return results
1196
1197
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001198class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001199 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001200 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001201 """
1202 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001203 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001204 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001205 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001206 gerrit_obj: provides basic Gerrit codereview functionality.
1207 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001208 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001209 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001210 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001211 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001212 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001213 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001214 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001215
1216 def ExecPresubmitScript(self, script_text, presubmit_path):
1217 """Executes a single presubmit script.
1218
1219 Args:
1220 script_text: The text of the presubmit script.
1221 presubmit_path: The path to the presubmit file (this will be reported via
1222 input_api.PresubmitLocalPath()).
1223
1224 Return:
1225 A list of result objects, empty if no problems.
1226 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001227
chase@chromium.org8e416c82009-10-06 04:30:44 +00001228 # Change to the presubmit file's directory to support local imports.
1229 main_path = os.getcwd()
1230 os.chdir(os.path.dirname(presubmit_path))
1231
1232 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001233 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001234 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001235 gerrit_obj=self.gerrit, dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001236 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001237 try:
1238 exec script_text in context
1239 except Exception, e:
1240 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001241
1242 # These function names must change if we make substantial changes to
1243 # the presubmit API that are not backwards compatible.
1244 if self.committing:
1245 function_name = 'CheckChangeOnCommit'
1246 else:
1247 function_name = 'CheckChangeOnUpload'
1248 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001249 context['__args'] = (input_api, OutputApi(self.committing))
tobiasjs2836bcf2016-08-16 04:08:16 -07001250 logging.debug('Running %s in %s', function_name, presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001251 result = eval(function_name + '(*__args)', context)
tobiasjs2836bcf2016-08-16 04:08:16 -07001252 logging.debug('Running %s done.', function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001253 if not (isinstance(result, types.TupleType) or
1254 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001255 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001256 'Presubmit functions must return a tuple or list')
1257 for item in result:
1258 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001259 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001260 'All presubmit results must be of types derived from '
1261 'output_api.PresubmitResult')
1262 else:
1263 result = () # no error since the script doesn't care about current event.
1264
scottmg86099d72016-09-01 09:16:51 -07001265 input_api.ShutdownPool()
1266
chase@chromium.org8e416c82009-10-06 04:30:44 +00001267 # Return the process to the original working directory.
1268 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001269 return result
1270
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001271def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001272 committing,
1273 verbose,
1274 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001275 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001276 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001277 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001278 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001279 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001280 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001281 """Runs all presubmit checks that apply to the files in the change.
1282
1283 This finds all PRESUBMIT.py files in directories enclosing the files in the
1284 change (up to the repository root) and calls the relevant entrypoint function
1285 depending on whether the change is being committed or uploaded.
1286
1287 Prints errors, warnings and notifications. Prompts the user for warnings
1288 when needed.
1289
1290 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001291 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001292 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001293 verbose: Prints debug info.
1294 output_stream: A stream to write output from presubmit tests to.
1295 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001296 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001297 may_prompt: Enable (y/n) questions on warning or error. If False,
1298 any questions are answered with yes by default.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001299 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001300 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001301 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001302
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001303 Warning:
1304 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1305 SHOULD be sys.stdin.
1306
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001307 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001308 A PresubmitOutput object. Use output.should_continue() to figure out
1309 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001310 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001311 old_environ = os.environ
1312 try:
1313 # Make sure python subprocesses won't generate .pyc files.
1314 os.environ = os.environ.copy()
1315 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001316
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001317 output = PresubmitOutput(input_stream, output_stream)
1318 if committing:
1319 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001320 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001321 output.write("Running presubmit upload checks ...\n")
1322 start_time = time.time()
1323 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001324 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001325 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001326 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001327 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001328 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001329 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001330 if default_presubmit:
1331 if verbose:
1332 output.write("Running default presubmit script.\n")
1333 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1334 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1335 for filename in presubmit_files:
1336 filename = os.path.abspath(filename)
1337 if verbose:
1338 output.write("Running %s\n" % filename)
1339 # Accept CRLF presubmit script.
1340 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1341 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001342
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001343 errors = []
1344 notifications = []
1345 warnings = []
1346 for result in results:
1347 if result.fatal:
1348 errors.append(result)
1349 elif result.should_prompt:
1350 warnings.append(result)
1351 else:
1352 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001353
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001354 output.write('\n')
1355 for name, items in (('Messages', notifications),
1356 ('Warnings', warnings),
1357 ('ERRORS', errors)):
1358 if items:
1359 output.write('** Presubmit %s **\n' % name)
1360 for item in items:
1361 item.handle(output)
1362 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001363
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001364 total_time = time.time() - start_time
1365 if total_time > 1.0:
1366 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001367
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001368 if errors:
1369 output.fail()
1370 elif warnings:
1371 output.write('There were presubmit warnings. ')
1372 if may_prompt:
1373 output.prompt_yes_no('Are you sure you wish to continue? (y/N): ')
1374 else:
1375 output.write('Presubmit checks passed.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001376
1377 global _ASKED_FOR_FEEDBACK
1378 # Ask for feedback one time out of 5.
1379 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001380 output.write(
1381 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1382 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1383 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001384 _ASKED_FOR_FEEDBACK = True
1385 return output
1386 finally:
1387 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001388
1389
1390def ScanSubDirs(mask, recursive):
1391 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001392 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001393
1394 results = []
1395 for root, dirs, files in os.walk('.'):
1396 if '.svn' in dirs:
1397 dirs.remove('.svn')
1398 if '.git' in dirs:
1399 dirs.remove('.git')
1400 for name in files:
1401 if fnmatch.fnmatch(name, mask):
1402 results.append(os.path.join(root, name))
1403 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001404
1405
1406def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001407 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001408 files = []
1409 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001410 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001411 return files
1412
1413
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001414def load_files(options, args):
1415 """Tries to determine the SCM."""
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001416 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001417 if args:
1418 files = ParseFiles(args, options.recursive)
agable0b65e732016-11-22 09:25:46 -08001419 change_scm = scm.determine_scm(options.root)
1420 if change_scm == 'git':
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001421 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001422 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001423 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001424 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001425 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001426 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001427 if not files:
1428 return None, None
1429 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001430 return change_class, files
1431
1432
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001433class NonexistantCannedCheckFilter(Exception):
1434 pass
1435
1436
1437@contextlib.contextmanager
1438def canned_check_filter(method_names):
1439 filtered = {}
1440 try:
1441 for method_name in method_names:
1442 if not hasattr(presubmit_canned_checks, method_name):
1443 raise NonexistantCannedCheckFilter(method_name)
1444 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1445 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1446 yield
1447 finally:
1448 for name, method in filtered.iteritems():
1449 setattr(presubmit_canned_checks, name, method)
1450
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001451
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001452def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001453 """Runs an external program, potentially from a child process created by the
1454 multiprocessing module.
1455
1456 multiprocessing needs a top level function with a single argument.
1457 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001458 cmd_data.kwargs['stdout'] = subprocess.PIPE
1459 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1460 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001461 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001462 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001463 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001464 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001465 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001466 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001467 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1468 if code != 0:
1469 return cmd_data.message(
1470 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1471 if cmd_data.info:
1472 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001473
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001474
sbc@chromium.org013731e2015-02-26 18:28:43 +00001475def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001476 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001477 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001478 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001479 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001480 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1481 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001482 parser.add_option("-r", "--recursive", action="store_true",
1483 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001484 parser.add_option("-v", "--verbose", action="count", default=0,
1485 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001486 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001487 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001488 parser.add_option("--description", default='')
1489 parser.add_option("--issue", type='int', default=0)
1490 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001491 parser.add_option("--root", default=os.getcwd(),
1492 help="Search for PRESUBMIT.py up to this directory. "
1493 "If inherit-review-settings-ok is present in this "
1494 "directory, parent directories up to the root file "
1495 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001496 parser.add_option("--upstream",
1497 help="Git only: the base ref or upstream branch against "
1498 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001499 parser.add_option("--default_presubmit")
1500 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001501 parser.add_option("--skip_canned", action='append', default=[],
1502 help="A list of checks to skip which appear in "
1503 "presubmit_canned_checks. Can be provided multiple times "
1504 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001505 parser.add_option("--dry_run", action='store_true',
1506 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001507 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001508 parser.add_option("--gerrit_fetch", action='store_true',
1509 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001510 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1511 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001512 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1513 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001514 # These are for OAuth2 authentication for bots. See also apply_issue.py
1515 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1516 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1517
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001518 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001519 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001520 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001521
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001522 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001523 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001524 elif options.verbose:
1525 logging.basicConfig(level=logging.INFO)
1526 else:
1527 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001528
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001529 if (any((options.rietveld_url, options.rietveld_email_file,
1530 options.rietveld_fetch, options.rietveld_private_key_file))
1531 and any((options.gerrit_url, options.gerrit_fetch))):
1532 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1533 'allowed')
1534
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001535 if options.rietveld_email and options.rietveld_email_file:
1536 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1537 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001538 if options.rietveld_email_file:
1539 with open(options.rietveld_email_file, "rb") as f:
1540 options.rietveld_email = f.read().strip()
1541
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001542 change_class, files = load_files(options, args)
1543 if not change_class:
1544 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001545 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001546
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001547 rietveld_obj, gerrit_obj = None, None
1548
maruel@chromium.org239f4112011-06-03 20:08:23 +00001549 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001550 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001551 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001552 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1553 options.rietveld_url,
1554 options.rietveld_email,
1555 options.rietveld_private_key_file)
1556 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001557 rietveld_obj = rietveld.CachingRietveld(
1558 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001559 auth_config,
1560 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001561 if options.rietveld_fetch:
1562 assert options.issue
1563 props = rietveld_obj.get_issue_properties(options.issue, False)
1564 options.author = props['owner_email']
1565 options.description = props['description']
1566 logging.info('Got author: "%s"', options.author)
1567 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001568
1569 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001570 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001571 rietveld_obj = None
1572 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1573 options.author = gerrit_obj.GetChangeOwner(options.issue)
1574 options.description = gerrit_obj.GetChangeDescription(options.issue,
1575 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001576 logging.info('Got author: "%s"', options.author)
1577 logging.info('Got description: """\n%s\n"""', options.description)
1578
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001579 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001580 with canned_check_filter(options.skip_canned):
1581 results = DoPresubmitChecks(
1582 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001583 options.description,
1584 options.root,
1585 files,
1586 options.issue,
1587 options.patchset,
1588 options.author,
1589 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001590 options.commit,
1591 options.verbose,
1592 sys.stdout,
1593 sys.stdin,
1594 options.default_presubmit,
1595 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001596 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001597 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001598 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001599 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001600 except NonexistantCannedCheckFilter, e:
1601 print >> sys.stderr, (
1602 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1603 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001604 except PresubmitFailure, e:
1605 print >> sys.stderr, e
1606 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1607 print >> sys.stderr, 'If all fails, contact maruel@'
1608 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001609
1610
1611if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001612 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001613 try:
1614 sys.exit(main())
1615 except KeyboardInterrupt:
1616 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001617 sys.exit(2)