blob: 0a2d691297c933aa89f6923014462d462593dfa8 [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
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +090015import ast # Exposed through the API.
enne@chromium.orge72c5f52013-04-16 00:36:40 +000016import cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000017import cPickle # Exposed through the API.
18import cStringIO # Exposed through the API.
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +000019import contextlib
dcheng091b7db2016-06-16 01:27:51 -070020import fnmatch # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000021import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000022import inspect
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +000023import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000024import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000025import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import marshal # Exposed through the API.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000027import multiprocessing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000028import optparse
29import os # Somewhat exposed through the API.
30import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000031import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000033import sys # Parts exposed through API.
34import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000035import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000036import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000037import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000038import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000039import urllib2 # Exposed through the API.
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000040import urlparse
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000041from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000042
43# Local imports.
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.org5aeb7dd2009-11-17 18:09:01 +000051import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000052import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053
54
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000055# Ask for feedback only once in program lifetime.
56_ASKED_FOR_FEEDBACK = False
57
58
maruel@chromium.org899e1c12011-04-07 17:03:18 +000059class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000060 pass
61
62
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000063class CommandData(object):
64 def __init__(self, name, cmd, kwargs, message):
65 self.name = name
66 self.cmd = cmd
67 self.kwargs = kwargs
68 self.message = message
69 self.info = None
70
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000071
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000072def normpath(path):
73 '''Version of os.path.normpath that also changes backward slashes to
74 forward slashes when not running on Windows.
75 '''
76 # This is safe to always do because the Windows version of os.path.normpath
77 # will replace forward slashes with backward slashes.
78 path = path.replace(os.sep, '/')
79 return os.path.normpath(path)
80
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000081
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000082def _RightHandSideLinesImpl(affected_files):
83 """Implements RightHandSideLines for InputApi and GclChange."""
84 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000085 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000086 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000087 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000088
89
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000090class PresubmitOutput(object):
91 def __init__(self, input_stream=None, output_stream=None):
92 self.input_stream = input_stream
93 self.output_stream = output_stream
94 self.reviewers = []
Daniel Cheng7227d212017-11-17 08:12:37 -080095 self.more_cc = []
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000096 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 []
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000138 self._long_text = long_text.rstrip()
139
140 def handle(self, output):
141 output.write(self._message)
142 output.write('\n')
143 for index, item in enumerate(self._items):
144 output.write(' ')
145 # Write separately in case it's unicode.
146 output.write(str(item))
147 if index < len(self._items) - 1:
148 output.write(' \\')
149 output.write('\n')
150 if self._long_text:
151 output.write('\n***************\n')
152 # Write separately in case it's unicode.
153 output.write(self._long_text)
154 output.write('\n***************\n')
155 if self.fatal:
156 output.fail()
157
158
159# Top level object so multiprocessing can pickle
160# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000161class _PresubmitError(_PresubmitResult):
162 """A hard presubmit error."""
163 fatal = True
164
165
166# Top level object so multiprocessing can pickle
167# Public access through OutputApi object.
168class _PresubmitPromptWarning(_PresubmitResult):
169 """An warning that prompts the user if they want to continue."""
170 should_prompt = True
171
172
173# Top level object so multiprocessing can pickle
174# Public access through OutputApi object.
175class _PresubmitNotifyResult(_PresubmitResult):
176 """Just print something to the screen -- but it's not even a warning."""
177 pass
178
179
180# Top level object so multiprocessing can pickle
181# Public access through OutputApi object.
182class _MailTextResult(_PresubmitResult):
183 """A warning that should be included in the review request email."""
184 def __init__(self, *args, **kwargs):
185 super(_MailTextResult, self).__init__()
186 raise NotImplementedError()
187
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000188class GerritAccessor(object):
189 """Limited Gerrit functionality for canned presubmit checks to work.
190
191 To avoid excessive Gerrit calls, caches the results.
192 """
193
194 def __init__(self, host):
195 self.host = host
196 self.cache = {}
197
198 def _FetchChangeDetail(self, issue):
199 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100200 try:
201 return gerrit_util.GetChangeDetail(
202 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700203 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100204 except gerrit_util.GerritError as e:
205 if e.http_status == 404:
206 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
207 'no credentials to fetch issue details' % issue)
208 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000209
210 def GetChangeInfo(self, issue):
211 """Returns labels and all revisions (patchsets) for this issue.
212
213 The result is a dictionary according to Gerrit REST Api.
214 https://gerrit-review.googlesource.com/Documentation/rest-api.html
215
216 However, API isn't very clear what's inside, so see tests for example.
217 """
218 assert issue
219 cache_key = int(issue)
220 if cache_key not in self.cache:
221 self.cache[cache_key] = self._FetchChangeDetail(issue)
222 return self.cache[cache_key]
223
224 def GetChangeDescription(self, issue, patchset=None):
225 """If patchset is none, fetches current patchset."""
226 info = self.GetChangeInfo(issue)
227 # info is a reference to cache. We'll modify it here adding description to
228 # it to the right patchset, if it is not yet there.
229
230 # Find revision info for the patchset we want.
231 if patchset is not None:
232 for rev, rev_info in info['revisions'].iteritems():
233 if str(rev_info['_number']) == str(patchset):
234 break
235 else:
236 raise Exception('patchset %s doesn\'t exist in issue %s' % (
237 patchset, issue))
238 else:
239 rev = info['current_revision']
240 rev_info = info['revisions'][rev]
241
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100242 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000243
Mun Yong Jang603d01e2017-12-19 16:38:30 -0800244 def GetDestRef(self, issue):
245 ref = self.GetChangeInfo(issue)['branch']
246 if not ref.startswith('refs/'):
247 # NOTE: it is possible to create 'refs/x' branch,
248 # aka 'refs/heads/refs/x'. However, this is ill-advised.
249 ref = 'refs/heads/%s' % ref
250 return ref
251
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000252 def GetChangeOwner(self, issue):
253 return self.GetChangeInfo(issue)['owner']['email']
254
255 def GetChangeReviewers(self, issue, approving_only=True):
Aaron Gable8b478f02017-07-31 15:33:19 -0700256 changeinfo = self.GetChangeInfo(issue)
257 if approving_only:
258 labelinfo = changeinfo.get('labels', {}).get('Code-Review', {})
259 values = labelinfo.get('values', {}).keys()
260 try:
261 max_value = max(int(v) for v in values)
262 reviewers = [r for r in labelinfo.get('all', [])
263 if r.get('value', 0) == max_value]
264 except ValueError: # values is the empty list
265 reviewers = []
266 else:
267 reviewers = changeinfo.get('reviewers', {}).get('REVIEWER', [])
268 return [r.get('email') for r in reviewers]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000269
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000270
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000271class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000272 """An instance of OutputApi gets passed to presubmit scripts so that they
273 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000274 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000275 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000276 PresubmitError = _PresubmitError
277 PresubmitPromptWarning = _PresubmitPromptWarning
278 PresubmitNotifyResult = _PresubmitNotifyResult
279 MailTextResult = _MailTextResult
280
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000281 def __init__(self, is_committing):
282 self.is_committing = is_committing
Daniel Cheng7227d212017-11-17 08:12:37 -0800283 self.more_cc = []
284
285 def AppendCC(self, cc):
286 """Appends a user to cc for this change."""
287 self.more_cc.append(cc)
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000288
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000289 def PresubmitPromptOrNotify(self, *args, **kwargs):
290 """Warn the user when uploading, but only notify if committing."""
291 if self.is_committing:
292 return self.PresubmitNotifyResult(*args, **kwargs)
293 return self.PresubmitPromptWarning(*args, **kwargs)
294
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800295 def EnsureCQIncludeTrybotsAreAdded(self, cl, bots_to_include, message):
296 """Helper for any PostUploadHook wishing to add CQ_INCLUDE_TRYBOTS.
297
298 Merges the bots_to_include into the current CQ_INCLUDE_TRYBOTS list,
299 keeping it alphabetically sorted. Returns the results that should be
300 returned from the PostUploadHook.
301
302 Args:
303 cl: The git_cl.Changelist object.
304 bots_to_include: A list of strings of bots to include, in the form
305 "master:slave".
306 message: A message to be printed in the case that
307 CQ_INCLUDE_TRYBOTS was updated.
308 """
309 description = cl.GetDescription(force=True)
Aaron Gableb584c4f2017-04-26 16:28:08 -0700310 include_re = re.compile(r'^CQ_INCLUDE_TRYBOTS=(.*)$', re.M | re.I)
311
312 prior_bots = []
313 if cl.IsGerrit():
314 trybot_footers = git_footers.parse_footers(description).get(
315 git_footers.normalize_name('Cq-Include-Trybots'), [])
316 for f in trybot_footers:
317 prior_bots += [b.strip() for b in f.split(';') if b.strip()]
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800318 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700319 trybot_tags = include_re.finditer(description)
320 for t in trybot_tags:
321 prior_bots += [b.strip() for b in t.group(1).split(';') if b.strip()]
322
323 if set(prior_bots) >= set(bots_to_include):
324 return []
325 all_bots = ';'.join(sorted(set(prior_bots) | set(bots_to_include)))
326
327 if cl.IsGerrit():
328 description = git_footers.remove_footer(
329 description, 'Cq-Include-Trybots')
330 description = git_footers.add_footer(
331 description, 'Cq-Include-Trybots', all_bots,
332 before_keys=['Change-Id'])
333 else:
334 new_include_trybots = 'CQ_INCLUDE_TRYBOTS=%s' % all_bots
335 m = include_re.search(description)
336 if m:
337 description = include_re.sub(new_include_trybots, description)
Kenneth Russelldf6e7342017-04-24 17:07:41 -0700338 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700339 description = '%s\n%s\n' % (description, new_include_trybots)
340
341 cl.UpdateDescription(description, force=True)
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800342 return [self.PresubmitNotifyResult(message)]
343
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000344
345class InputApi(object):
346 """An instance of this object is passed to presubmit scripts so they can
347 know stuff about the change they're looking at.
348 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000349 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800350 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000351
maruel@chromium.org3410d912009-06-09 20:56:16 +0000352 # File extensions that are considered source files from a style guide
353 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000354 #
355 # Files without an extension aren't included in the list. If you want to
356 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
357 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000358 DEFAULT_WHITE_LIST = (
359 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000360 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
361 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000362 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000363 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000364 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000365 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000366 )
367
368 # Path regexp that should be excluded from being considered containing source
369 # files. Don't modify this list from a presubmit script!
370 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000371 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000372 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000373 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
374 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000375 # Output directories (just in case)
376 r".*\bDebug[\\\/].*",
377 r".*\bRelease[\\\/].*",
378 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000379 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000380 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000381 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000382 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000383 r"(|.*[\\\/])\.git[\\\/].*",
384 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000385 # There is no point in processing a patch file.
386 r".+\.diff$",
387 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000388 )
389
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000390 def __init__(self, change, presubmit_path, is_committing,
Edward Lesmes1ad681e2018-04-03 17:15:57 -0400391 verbose, gerrit_obj, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000392 """Builds an InputApi object.
393
394 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000395 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000396 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000397 is_committing: True if the change is about to be committed.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000398 gerrit_obj: provides basic Gerrit codereview functionality.
399 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000400 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000401 # Version number of the presubmit_support script.
402 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000403 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000404 self.is_committing = is_committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000405 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000406 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000407
408 # We expose various modules and functions as attributes of the input_api
409 # so that presubmit scripts don't have to import them.
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +0900410 self.ast = ast
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000411 self.basename = os.path.basename
412 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000413 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000414 self.cStringIO = cStringIO
dcheng091b7db2016-06-16 01:27:51 -0700415 self.fnmatch = fnmatch
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000416 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000417 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000418 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000419 self.os_listdir = os.listdir
420 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000421 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000422 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000423 self.pickle = pickle
424 self.marshal = marshal
425 self.re = re
426 self.subprocess = subprocess
427 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000428 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000429 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000430 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000431 self.urllib2 = urllib2
432
Robert Iannucci50258932018-03-19 10:30:59 -0700433 self.is_windows = sys.platform == 'win32'
434
435 # Set python_executable to 'python'. This is interpreted in CallCommand to
436 # convert to vpython in order to allow scripts in other repos (e.g. src.git)
437 # to automatically pick up that repo's .vpython file, instead of inheriting
438 # the one in depot_tools.
439 self.python_executable = 'python'
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000440 self.environ = os.environ
441
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000442 # InputApi.platform is the platform you're currently running on.
443 self.platform = sys.platform
444
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000445 self.cpu_count = multiprocessing.cpu_count()
446
Edward Lesmes1ad681e2018-04-03 17:15:57 -0400447 # this is done here because in RunTests, the current working directory has
448 # changed, which causes Pool() to explode fantastically when run on windows
449 # (because it tries to load the __main__ module, which imports lots of
450 # things relative to the current working directory).
451 self._run_tests_pool = multiprocessing.Pool(self.cpu_count)
452
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000453 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000454 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000455
456 # We carry the canned checks so presubmit scripts can easily use them.
457 self.canned_checks = presubmit_canned_checks
458
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100459 # Temporary files we must manually remove at the end of a run.
460 self._named_temporary_files = []
Jochen Eisinger72606f82017-04-04 10:44:18 +0200461
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000462 # TODO(dpranke): figure out a list of all approved owners for a repo
463 # in order to be able to handle wildcard OWNERS files?
464 self.owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +0200465 fopen=file, os_path=self.os_path)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200466 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000467 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000468 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000469
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000470 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000471 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000472 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800473 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000474 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000475 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000476 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
477 for (a, b, header) in cpplint._re_pattern_templates
478 ]
479
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000480 def PresubmitLocalPath(self):
481 """Returns the local path of the presubmit script currently being run.
482
483 This is useful if you don't want to hard-code absolute paths in the
484 presubmit script. For example, It can be used to find another file
485 relative to the PRESUBMIT.py script, so the whole tree can be branched and
486 the presubmit script still works, without editing its content.
487 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000488 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000489
agable0b65e732016-11-22 09:25:46 -0800490 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000491 """Same as input_api.change.AffectedFiles() except only lists files
492 (and optionally directories) in the same directory as the current presubmit
493 script, or subdirectories thereof.
494 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000495 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000496 if len(dir_with_slash) == 1:
497 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000498
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000499 return filter(
500 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
agable0b65e732016-11-22 09:25:46 -0800501 self.change.AffectedFiles(include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000502
agable0b65e732016-11-22 09:25:46 -0800503 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000504 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800505 paths = [af.LocalPath() for af in self.AffectedFiles()]
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000506 logging.debug("LocalPaths: %s", paths)
507 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000508
agable0b65e732016-11-22 09:25:46 -0800509 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000510 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800511 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000512
agable0b65e732016-11-22 09:25:46 -0800513 def AffectedTestableFiles(self, include_deletes=None):
514 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000515 in the same directory as the current presubmit script, or subdirectories
516 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000517 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000518 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800519 warn("AffectedTestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000520 " is deprecated and ignored" % str(include_deletes),
521 category=DeprecationWarning,
522 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800523 return filter(lambda x: x.IsTestableFile(),
524 self.AffectedFiles(include_deletes=False))
525
526 def AffectedTextFiles(self, include_deletes=None):
527 """An alias to AffectedTestableFiles for backwards compatibility."""
528 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000529
maruel@chromium.org3410d912009-06-09 20:56:16 +0000530 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
531 """Filters out files that aren't considered "source file".
532
533 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
534 and InputApi.DEFAULT_BLACK_LIST is used respectively.
535
536 The lists will be compiled as regular expression and
537 AffectedFile.LocalPath() needs to pass both list.
538
539 Note: Copy-paste this function to suit your needs or use a lambda function.
540 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000541 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000542 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000543 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000544 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000545 return True
546 return False
547 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
548 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
549
550 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800551 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000552
553 If source_file is None, InputApi.FilterSourceFile() is used.
554 """
555 if not source_file:
556 source_file = self.FilterSourceFile
agable0b65e732016-11-22 09:25:46 -0800557 return filter(source_file, self.AffectedTestableFiles())
maruel@chromium.org3410d912009-06-09 20:56:16 +0000558
559 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000560 """An iterator over all text lines in "new" version of changed files.
561
562 Only lists lines from new or modified text files in the change that are
563 contained by the directory of the currently executing presubmit script.
564
565 This is useful for doing line-by-line regex checks, like checking for
566 trailing whitespace.
567
568 Yields:
569 a 3 tuple:
570 the AffectedFile instance of the current file;
571 integer line number (1-based); and
572 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000573
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000574 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000575 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000576 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000577 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000578
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000579 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000580 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000581
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000582 Deny reading anything outside the repository.
583 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000584 if isinstance(file_item, AffectedFile):
585 file_item = file_item.AbsoluteLocalPath()
586 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000587 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000588 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000589
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100590 def CreateTemporaryFile(self, **kwargs):
591 """Returns a named temporary file that must be removed with a call to
592 RemoveTemporaryFiles().
593
594 All keyword arguments are forwarded to tempfile.NamedTemporaryFile(),
595 except for |delete|, which is always set to False.
596
597 Presubmit checks that need to create a temporary file and pass it for
598 reading should use this function instead of NamedTemporaryFile(), as
599 Windows fails to open a file that is already open for writing.
600
601 with input_api.CreateTemporaryFile() as f:
602 f.write('xyz')
603 f.close()
604 input_api.subprocess.check_output(['script-that', '--reads-from',
605 f.name])
606
607
608 Note that callers of CreateTemporaryFile() should not worry about removing
609 any temporary file; this is done transparently by the presubmit handling
610 code.
611 """
612 if 'delete' in kwargs:
613 # Prevent users from passing |delete|; we take care of file deletion
614 # ourselves and this prevents unintuitive error messages when we pass
615 # delete=False and 'delete' is also in kwargs.
616 raise TypeError('CreateTemporaryFile() does not take a "delete" '
617 'argument, file deletion is handled automatically by '
618 'the same presubmit_support code that creates InputApi '
619 'objects.')
620 temp_file = self.tempfile.NamedTemporaryFile(delete=False, **kwargs)
621 self._named_temporary_files.append(temp_file.name)
622 return temp_file
623
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000624 @property
625 def tbr(self):
626 """Returns if a change is TBR'ed."""
Jeremy Romandce22502017-06-20 15:37:29 -0400627 return 'TBR' in self.change.tags or self.change.TBRsFromDescription()
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000628
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000629 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000630 tests = []
631 msgs = []
632 for t in tests_mix:
Edward Lesmes1ad681e2018-04-03 17:15:57 -0400633 if isinstance(t, OutputApi.PresubmitResult):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000634 msgs.append(t)
635 else:
636 assert issubclass(t.message, _PresubmitResult)
637 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000638 if self.verbose:
639 t.info = _PresubmitNotifyResult
Edward Lesmes1ad681e2018-04-03 17:15:57 -0400640 if len(tests) > 1 and parallel:
641 # async recipe works around multiprocessing bug handling Ctrl-C
642 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
643 else:
644 msgs.extend(map(CallCommand, tests))
645 return [m for m in msgs if m]
646
647 def ShutdownPool(self):
648 self._run_tests_pool.close()
649 self._run_tests_pool.join()
650 self._run_tests_pool = None
scottmg86099d72016-09-01 09:16:51 -0700651
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000652
nick@chromium.orgff526192013-06-10 19:30:26 +0000653class _DiffCache(object):
654 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000655 def __init__(self, upstream=None):
656 """Stores the upstream revision against which all diffs will be computed."""
657 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000658
659 def GetDiff(self, path, local_root):
660 """Get the diff for a particular path."""
661 raise NotImplementedError()
662
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700663 def GetOldContents(self, path, local_root):
664 """Get the old version for a particular path."""
665 raise NotImplementedError()
666
nick@chromium.orgff526192013-06-10 19:30:26 +0000667
nick@chromium.orgff526192013-06-10 19:30:26 +0000668class _GitDiffCache(_DiffCache):
669 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000670 def __init__(self, upstream):
671 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000672 self._diffs_by_file = None
673
674 def GetDiff(self, path, local_root):
675 if not self._diffs_by_file:
676 # Compute a single diff for all files and parse the output; should
677 # with git this is much faster than computing one diff for each file.
678 diffs = {}
679
680 # Don't specify any filenames below, because there are command line length
681 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000682 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
683 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000684
685 # This regex matches the path twice, separated by a space. Note that
686 # filename itself may contain spaces.
687 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
688 current_diff = []
689 keep_line_endings = True
690 for x in unified_diff.splitlines(keep_line_endings):
691 match = file_marker.match(x)
692 if match:
693 # Marks the start of a new per-file section.
694 diffs[match.group('filename')] = current_diff = [x]
695 elif x.startswith('diff --git'):
696 raise PresubmitFailure('Unexpected diff line: %s' % x)
697 else:
698 current_diff.append(x)
699
700 self._diffs_by_file = dict(
701 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
702
703 if path not in self._diffs_by_file:
704 raise PresubmitFailure(
705 'Unified diff did not contain entry for file %s' % path)
706
707 return self._diffs_by_file[path]
708
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700709 def GetOldContents(self, path, local_root):
710 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
711
nick@chromium.orgff526192013-06-10 19:30:26 +0000712
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000713class AffectedFile(object):
714 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000715
716 DIFF_CACHE = _DiffCache
717
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000718 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800719 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000720 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000721 self._path = path
722 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000723 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000724 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000725 self._cached_changed_contents = None
726 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000727 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700728 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000729
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000730 def LocalPath(self):
731 """Returns the path of this file on the local disk relative to client root.
Andrew Grieve92b8b992017-11-02 09:42:24 -0400732
733 This should be used for error messages but not for accessing files,
734 because presubmit checks are run with CWD=PresubmitLocalPath() (which is
735 often != client root).
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000736 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000737 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000738
739 def AbsoluteLocalPath(self):
740 """Returns the absolute path of this file on the local disk.
741 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000742 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000743
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000744 def Action(self):
745 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000746 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000747
agable0b65e732016-11-22 09:25:46 -0800748 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000749 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000750
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000751 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000752 raise NotImplementedError() # Implement when needed
753
agable0b65e732016-11-22 09:25:46 -0800754 def IsTextFile(self):
755 """An alias to IsTestableFile for backwards compatibility."""
756 return self.IsTestableFile()
757
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700758 def OldContents(self):
759 """Returns an iterator over the lines in the old version of file.
760
Daniel Cheng2da34fe2017-03-21 20:42:12 -0700761 The old version is the file before any modifications in the user's
762 workspace, i.e. the "left hand side".
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700763
764 Contents will be empty if the file is a directory or does not exist.
765 Note: The carriage returns (LF or CR) are stripped off.
766 """
767 return self._diff_cache.GetOldContents(self.LocalPath(),
768 self._local_root).splitlines()
769
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000770 def NewContents(self):
771 """Returns an iterator over the lines in the new version of file.
772
773 The new version is the file in the user's workspace, i.e. the "right hand
774 side".
775
776 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000777 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000778 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000779 if self._cached_new_contents is None:
780 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800781 try:
782 self._cached_new_contents = gclient_utils.FileRead(
783 self.AbsoluteLocalPath(), 'rU').splitlines()
784 except IOError:
785 pass # File not found? That's fine; maybe it was deleted.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000786 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000787
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000788 def ChangedContents(self):
789 """Returns a list of tuples (line number, line text) of all new lines.
790
791 This relies on the scm diff output describing each changed code section
792 with a line of the form
793
794 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
795 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000796 if self._cached_changed_contents is not None:
797 return self._cached_changed_contents[:]
798 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000799 line_num = 0
800
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000801 for line in self.GenerateScmDiff().splitlines():
802 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
803 if m:
804 line_num = int(m.groups(1)[0])
805 continue
806 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000807 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000808 if not line.startswith('-'):
809 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000810 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000811
maruel@chromium.org5de13972009-06-10 18:16:06 +0000812 def __str__(self):
813 return self.LocalPath()
814
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000815 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000816 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000817
maruel@chromium.org58407af2011-04-12 23:15:57 +0000818
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000819class GitAffectedFile(AffectedFile):
820 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000821 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800822 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000823
nick@chromium.orgff526192013-06-10 19:30:26 +0000824 DIFF_CACHE = _GitDiffCache
825
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000826 def __init__(self, *args, **kwargs):
827 AffectedFile.__init__(self, *args, **kwargs)
828 self._server_path = None
agable0b65e732016-11-22 09:25:46 -0800829 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000830
agable0b65e732016-11-22 09:25:46 -0800831 def IsTestableFile(self):
832 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000833 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -0800834 # A deleted file is not testable.
835 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000836 else:
agable0b65e732016-11-22 09:25:46 -0800837 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
838 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000839
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000840
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000841class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000842 """Describe a change.
843
844 Used directly by the presubmit scripts to query the current change being
845 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000846
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000847 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000848 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000849 self.KEY: equivalent to tags['KEY']
850 """
851
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000852 _AFFECTED_FILES = AffectedFile
853
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000854 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000855 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000856 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000857 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000858
maruel@chromium.org58407af2011-04-12 23:15:57 +0000859 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000860 self, name, description, local_root, files, issue, patchset, author,
861 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000862 if files is None:
863 files = []
864 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000865 # Convert root into an absolute path.
866 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000867 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000868 self.issue = issue
869 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000870 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000871
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000872 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000873 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000874 self._description_without_tags = ''
875 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000876
maruel@chromium.orge085d812011-10-10 19:49:15 +0000877 assert all(
878 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
879
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000880 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000881 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000882 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
883 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000884 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000885
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000886 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000887 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000888 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000889
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000890 def DescriptionText(self):
891 """Returns the user-entered changelist description, minus tags.
892
893 Any line in the user-provided description starting with e.g. "FOO="
894 (whitespace permitted before and around) is considered a tag line. Such
895 lines are stripped out of the description this function returns.
896 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000897 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000898
899 def FullDescriptionText(self):
900 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000901 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000902
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000903 def SetDescriptionText(self, description):
904 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000905
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000906 Also updates the list of tags."""
907 self._full_description = description
908
909 # From the description text, build up a dictionary of key/value pairs
910 # plus the description minus all key/value or "tag" lines.
911 description_without_tags = []
912 self.tags = {}
913 for line in self._full_description.splitlines():
914 m = self.TAG_LINE_RE.match(line)
915 if m:
916 self.tags[m.group('key')] = m.group('value')
917 else:
918 description_without_tags.append(line)
919
920 # Change back to text and remove whitespace at end.
921 self._description_without_tags = (
922 '\n'.join(description_without_tags).rstrip())
923
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000924 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000925 """Returns the repository (checkout) root directory for this change,
926 as an absolute path.
927 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000928 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000929
930 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000931 """Return tags directly as attributes on the object."""
932 if not re.match(r"^[A-Z_]*$", attr):
933 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000934 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000935
Aaron Gablefc03e672017-05-15 14:09:42 -0700936 def BugsFromDescription(self):
937 """Returns all bugs referenced in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700938 tags = [b.strip() for b in self.tags.get('BUG', '').split(',') if b.strip()]
939 footers = git_footers.parse_footers(self._full_description).get('Bug', [])
940 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -0700941
942 def ReviewersFromDescription(self):
943 """Returns all reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700944 # We don't support a "R:" git-footer for reviewers; that is in metadata.
945 tags = [r.strip() for r in self.tags.get('R', '').split(',') if r.strip()]
946 return sorted(set(tags))
Aaron Gablefc03e672017-05-15 14:09:42 -0700947
948 def TBRsFromDescription(self):
949 """Returns all TBR reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700950 tags = [r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()]
951 # TODO(agable): Remove support for 'Tbr:' when TBRs are programmatically
952 # determined by self-CR+1s.
953 footers = git_footers.parse_footers(self._full_description).get('Tbr', [])
954 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -0700955
956 # TODO(agable): Delete these once we're sure they're unused.
957 @property
958 def BUG(self):
959 return ','.join(self.BugsFromDescription())
960 @property
961 def R(self):
962 return ','.join(self.ReviewersFromDescription())
963 @property
964 def TBR(self):
965 return ','.join(self.TBRsFromDescription())
966
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000967 def AllFiles(self, root=None):
968 """List all files under source control in the repo."""
969 raise NotImplementedError()
970
agable0b65e732016-11-22 09:25:46 -0800971 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000972 """Returns a list of AffectedFile instances for all files in the change.
973
974 Args:
975 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +0000976 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000977
978 Returns:
979 [AffectedFile(path, action), AffectedFile(path, action)]
980 """
agable0b65e732016-11-22 09:25:46 -0800981 affected = filter(file_filter, self._affected_files)
sail@chromium.org5538e022011-05-12 17:53:16 +0000982
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000983 if include_deletes:
984 return affected
Lei Zhang9611c4c2017-04-04 01:41:56 -0700985 return filter(lambda x: x.Action() != 'D', affected)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000986
agable0b65e732016-11-22 09:25:46 -0800987 def AffectedTestableFiles(self, include_deletes=None):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000988 """Return a list of the existing text files in a change."""
989 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800990 warn("AffectedTeestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000991 " is deprecated and ignored" % str(include_deletes),
992 category=DeprecationWarning,
993 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800994 return filter(lambda x: x.IsTestableFile(),
995 self.AffectedFiles(include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000996
agable0b65e732016-11-22 09:25:46 -0800997 def AffectedTextFiles(self, include_deletes=None):
998 """An alias to AffectedTestableFiles for backwards compatibility."""
999 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001000
agable0b65e732016-11-22 09:25:46 -08001001 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001002 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001003 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001004
agable0b65e732016-11-22 09:25:46 -08001005 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001007 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008
1009 def RightHandSideLines(self):
1010 """An iterator over all text lines in "new" version of changed files.
1011
1012 Lists lines from new or modified text files in the change.
1013
1014 This is useful for doing line-by-line regex checks, like checking for
1015 trailing whitespace.
1016
1017 Yields:
1018 a 3 tuple:
1019 the AffectedFile instance of the current file;
1020 integer line number (1-based); and
1021 the contents of the line as a string.
1022 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001023 return _RightHandSideLinesImpl(
1024 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -08001025 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001026
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02001027 def OriginalOwnersFiles(self):
1028 """A map from path names of affected OWNERS files to their old content."""
1029 def owners_file_filter(f):
1030 return 'OWNERS' in os.path.split(f.LocalPath())[1]
1031 files = self.AffectedFiles(file_filter=owners_file_filter)
1032 return dict([(f.LocalPath(), f.OldContents()) for f in files])
1033
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001034
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001035class GitChange(Change):
1036 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001037 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001038
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001039 def AllFiles(self, root=None):
1040 """List all files under source control in the repo."""
1041 root = root or self.RepositoryRoot()
1042 return subprocess.check_output(
Aaron Gable7817f022017-12-12 09:43:17 -08001043 ['git', '-c', 'core.quotePath=false', 'ls-files', '--', '.'],
1044 cwd=root).splitlines()
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001045
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001046
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001047def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001048 """Finds all presubmit files that apply to a given set of source files.
1049
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001050 If inherit-review-settings-ok is present right under root, looks for
1051 PRESUBMIT.py in directories enclosing root.
1052
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001053 Args:
1054 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001055 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001056
1057 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001058 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001059 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001060 files = [normpath(os.path.join(root, f)) for f in files]
1061
1062 # List all the individual directories containing files.
1063 directories = set([os.path.dirname(f) for f in files])
1064
1065 # Ignore root if inherit-review-settings-ok is present.
1066 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1067 root = None
1068
1069 # Collect all unique directories that may contain PRESUBMIT.py.
1070 candidates = set()
1071 for directory in directories:
1072 while True:
1073 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001074 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001075 candidates.add(directory)
1076 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001077 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001078 parent_dir = os.path.dirname(directory)
1079 if parent_dir == directory:
1080 # We hit the system root directory.
1081 break
1082 directory = parent_dir
1083
1084 # Look for PRESUBMIT.py in all candidate directories.
1085 results = []
1086 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -07001087 try:
1088 for f in os.listdir(directory):
1089 p = os.path.join(directory, f)
1090 if os.path.isfile(p) and re.match(
1091 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1092 results.append(p)
1093 except OSError:
1094 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001095
tobiasjs2836bcf2016-08-16 04:08:16 -07001096 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001097 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001098
1099
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001100class GetTryMastersExecuter(object):
1101 @staticmethod
1102 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1103 """Executes GetPreferredTryMasters() from a single presubmit script.
1104
1105 Args:
1106 script_text: The text of the presubmit script.
1107 presubmit_path: Project script to run.
1108 project: Project name to pass to presubmit script for bot selection.
1109
1110 Return:
1111 A map of try masters to map of builders to set of tests.
1112 """
1113 context = {}
1114 try:
1115 exec script_text in context
1116 except Exception, e:
1117 raise PresubmitFailure('"%s" had an exception.\n%s'
1118 % (presubmit_path, e))
1119
1120 function_name = 'GetPreferredTryMasters'
1121 if function_name not in context:
1122 return {}
1123 get_preferred_try_masters = context[function_name]
1124 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1125 raise PresubmitFailure(
1126 'Expected function "GetPreferredTryMasters" to take two arguments.')
1127 return get_preferred_try_masters(project, change)
1128
1129
rmistry@google.com5626a922015-02-26 14:03:30 +00001130class GetPostUploadExecuter(object):
1131 @staticmethod
1132 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1133 """Executes PostUploadHook() from a single presubmit script.
1134
1135 Args:
1136 script_text: The text of the presubmit script.
1137 presubmit_path: Project script to run.
1138 cl: The Changelist object.
1139 change: The Change object.
1140
1141 Return:
1142 A list of results objects.
1143 """
1144 context = {}
1145 try:
1146 exec script_text in context
1147 except Exception, e:
1148 raise PresubmitFailure('"%s" had an exception.\n%s'
1149 % (presubmit_path, e))
1150
1151 function_name = 'PostUploadHook'
1152 if function_name not in context:
1153 return {}
1154 post_upload_hook = context[function_name]
1155 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1156 raise PresubmitFailure(
1157 'Expected function "PostUploadHook" to take three arguments.')
1158 return post_upload_hook(cl, change, OutputApi(False))
1159
1160
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001161def _MergeMasters(masters1, masters2):
1162 """Merges two master maps. Merges also the tests of each builder."""
1163 result = {}
1164 for (master, builders) in itertools.chain(masters1.iteritems(),
1165 masters2.iteritems()):
1166 new_builders = result.setdefault(master, {})
1167 for (builder, tests) in builders.iteritems():
1168 new_builders.setdefault(builder, set([])).update(tests)
1169 return result
1170
1171
1172def DoGetTryMasters(change,
1173 changed_files,
1174 repository_root,
1175 default_presubmit,
1176 project,
1177 verbose,
1178 output_stream):
1179 """Get the list of try masters from the presubmit scripts.
1180
1181 Args:
1182 changed_files: List of modified files.
1183 repository_root: The repository root.
1184 default_presubmit: A default presubmit script to execute in any case.
1185 project: Optional name of a project used in selecting trybots.
1186 verbose: Prints debug info.
1187 output_stream: A stream to write debug output to.
1188
1189 Return:
1190 Map of try masters to map of builders to set of tests.
1191 """
1192 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1193 if not presubmit_files and verbose:
1194 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1195 results = {}
1196 executer = GetTryMastersExecuter()
1197
1198 if default_presubmit:
1199 if verbose:
1200 output_stream.write("Running default presubmit script.\n")
1201 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1202 results = _MergeMasters(results, executer.ExecPresubmitScript(
1203 default_presubmit, fake_path, project, change))
1204 for filename in presubmit_files:
1205 filename = os.path.abspath(filename)
1206 if verbose:
1207 output_stream.write("Running %s\n" % filename)
1208 # Accept CRLF presubmit script.
1209 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1210 results = _MergeMasters(results, executer.ExecPresubmitScript(
1211 presubmit_script, filename, project, change))
1212
1213 # Make sets to lists again for later JSON serialization.
1214 for builders in results.itervalues():
1215 for builder in builders:
1216 builders[builder] = list(builders[builder])
1217
1218 if results and verbose:
1219 output_stream.write('%s\n' % str(results))
1220 return results
1221
1222
rmistry@google.com5626a922015-02-26 14:03:30 +00001223def DoPostUploadExecuter(change,
1224 cl,
1225 repository_root,
1226 verbose,
1227 output_stream):
1228 """Execute the post upload hook.
1229
1230 Args:
1231 change: The Change object.
1232 cl: The Changelist object.
1233 repository_root: The repository root.
1234 verbose: Prints debug info.
1235 output_stream: A stream to write debug output to.
1236 """
1237 presubmit_files = ListRelevantPresubmitFiles(
1238 change.LocalPaths(), repository_root)
1239 if not presubmit_files and verbose:
1240 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1241 results = []
1242 executer = GetPostUploadExecuter()
1243 # The root presubmit file should be executed after the ones in subdirectories.
1244 # i.e. the specific post upload hooks should run before the general ones.
1245 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1246 presubmit_files.reverse()
1247
1248 for filename in presubmit_files:
1249 filename = os.path.abspath(filename)
1250 if verbose:
1251 output_stream.write("Running %s\n" % filename)
1252 # Accept CRLF presubmit script.
1253 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1254 results.extend(executer.ExecPresubmitScript(
1255 presubmit_script, filename, cl, change))
1256 output_stream.write('\n')
1257 if results:
1258 output_stream.write('** Post Upload Hook Messages **\n')
1259 for result in results:
1260 result.handle(output_stream)
1261 output_stream.write('\n')
1262
1263 return results
1264
1265
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001266class PresubmitExecuter(object):
Aaron Gable668c1d82018-04-03 10:19:16 -07001267 def __init__(self, change, committing, verbose,
Edward Lesmes1ad681e2018-04-03 17:15:57 -04001268 gerrit_obj, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001269 """
1270 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001271 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001272 committing: True if 'git cl land' is running, False if 'git cl upload' is.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001273 gerrit_obj: provides basic Gerrit codereview functionality.
1274 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001275 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001276 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001277 self.committing = committing
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001278 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001279 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001280 self.dry_run = dry_run
Daniel Cheng7227d212017-11-17 08:12:37 -08001281 self.more_cc = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001282
1283 def ExecPresubmitScript(self, script_text, presubmit_path):
1284 """Executes a single presubmit script.
1285
1286 Args:
1287 script_text: The text of the presubmit script.
1288 presubmit_path: The path to the presubmit file (this will be reported via
1289 input_api.PresubmitLocalPath()).
1290
1291 Return:
1292 A list of result objects, empty if no problems.
1293 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001294
chase@chromium.org8e416c82009-10-06 04:30:44 +00001295 # Change to the presubmit file's directory to support local imports.
1296 main_path = os.getcwd()
1297 os.chdir(os.path.dirname(presubmit_path))
1298
1299 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001300 input_api = InputApi(self.change, presubmit_path, self.committing,
Aaron Gable668c1d82018-04-03 10:19:16 -07001301 self.verbose, gerrit_obj=self.gerrit,
Edward Lesmes1ad681e2018-04-03 17:15:57 -04001302 dry_run=self.dry_run)
Daniel Cheng7227d212017-11-17 08:12:37 -08001303 output_api = OutputApi(self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001304 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001305 try:
1306 exec script_text in context
1307 except Exception, e:
1308 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001309
1310 # These function names must change if we make substantial changes to
1311 # the presubmit API that are not backwards compatible.
1312 if self.committing:
1313 function_name = 'CheckChangeOnCommit'
1314 else:
1315 function_name = 'CheckChangeOnUpload'
1316 if function_name in context:
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +01001317 try:
Daniel Cheng7227d212017-11-17 08:12:37 -08001318 context['__args'] = (input_api, output_api)
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +01001319 logging.debug('Running %s in %s', function_name, presubmit_path)
1320 result = eval(function_name + '(*__args)', context)
1321 logging.debug('Running %s done.', function_name)
Daniel Chengd36fce42017-11-21 21:52:52 -08001322 self.more_cc.extend(output_api.more_cc)
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +01001323 finally:
1324 map(os.remove, input_api._named_temporary_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001325 if not (isinstance(result, types.TupleType) or
1326 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001327 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001328 'Presubmit functions must return a tuple or list')
1329 for item in result:
1330 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001331 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001332 'All presubmit results must be of types derived from '
1333 'output_api.PresubmitResult')
1334 else:
1335 result = () # no error since the script doesn't care about current event.
1336
Edward Lesmes1ad681e2018-04-03 17:15:57 -04001337 input_api.ShutdownPool()
1338
chase@chromium.org8e416c82009-10-06 04:30:44 +00001339 # Return the process to the original working directory.
1340 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001341 return result
1342
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001343def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001344 committing,
1345 verbose,
1346 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001347 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001348 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001349 may_prompt,
Aaron Gable668c1d82018-04-03 10:19:16 -07001350 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001351 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001352 """Runs all presubmit checks that apply to the files in the change.
1353
1354 This finds all PRESUBMIT.py files in directories enclosing the files in the
1355 change (up to the repository root) and calls the relevant entrypoint function
1356 depending on whether the change is being committed or uploaded.
1357
1358 Prints errors, warnings and notifications. Prompts the user for warnings
1359 when needed.
1360
1361 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001362 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001363 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001364 verbose: Prints debug info.
1365 output_stream: A stream to write output from presubmit tests to.
1366 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001367 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001368 may_prompt: Enable (y/n) questions on warning or error. If False,
1369 any questions are answered with yes by default.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001370 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001371 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001372
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001373 Warning:
1374 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1375 SHOULD be sys.stdin.
1376
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001377 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001378 A PresubmitOutput object. Use output.should_continue() to figure out
1379 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001380 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001381 old_environ = os.environ
1382 try:
1383 # Make sure python subprocesses won't generate .pyc files.
1384 os.environ = os.environ.copy()
1385 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001386
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001387 output = PresubmitOutput(input_stream, output_stream)
1388 if committing:
1389 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001390 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001391 output.write("Running presubmit upload checks ...\n")
1392 start_time = time.time()
1393 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001394 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001395 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001396 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001397 results = []
Aaron Gable668c1d82018-04-03 10:19:16 -07001398 executer = PresubmitExecuter(change, committing, verbose,
Edward Lesmes1ad681e2018-04-03 17:15:57 -04001399 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001400 if default_presubmit:
1401 if verbose:
1402 output.write("Running default presubmit script.\n")
1403 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1404 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1405 for filename in presubmit_files:
1406 filename = os.path.abspath(filename)
1407 if verbose:
1408 output.write("Running %s\n" % filename)
1409 # Accept CRLF presubmit script.
1410 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1411 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001412
Daniel Cheng7227d212017-11-17 08:12:37 -08001413 output.more_cc.extend(executer.more_cc)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001414 errors = []
1415 notifications = []
1416 warnings = []
1417 for result in results:
1418 if result.fatal:
1419 errors.append(result)
1420 elif result.should_prompt:
1421 warnings.append(result)
1422 else:
1423 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001424
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001425 output.write('\n')
1426 for name, items in (('Messages', notifications),
1427 ('Warnings', warnings),
1428 ('ERRORS', errors)):
1429 if items:
1430 output.write('** Presubmit %s **\n' % name)
1431 for item in items:
1432 item.handle(output)
1433 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001434
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001435 total_time = time.time() - start_time
1436 if total_time > 1.0:
1437 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001438
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001439 if errors:
1440 output.fail()
1441 elif warnings:
1442 output.write('There were presubmit warnings. ')
1443 if may_prompt:
1444 output.prompt_yes_no('Are you sure you wish to continue? (y/N): ')
1445 else:
1446 output.write('Presubmit checks passed.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001447
1448 global _ASKED_FOR_FEEDBACK
1449 # Ask for feedback one time out of 5.
1450 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001451 output.write(
1452 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1453 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1454 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001455 _ASKED_FOR_FEEDBACK = True
1456 return output
1457 finally:
1458 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001459
1460
1461def ScanSubDirs(mask, recursive):
1462 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001463 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001464
1465 results = []
1466 for root, dirs, files in os.walk('.'):
1467 if '.svn' in dirs:
1468 dirs.remove('.svn')
1469 if '.git' in dirs:
1470 dirs.remove('.git')
1471 for name in files:
1472 if fnmatch.fnmatch(name, mask):
1473 results.append(os.path.join(root, name))
1474 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001475
1476
1477def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001478 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001479 files = []
1480 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001481 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001482 return files
1483
1484
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001485def load_files(options, args):
1486 """Tries to determine the SCM."""
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001487 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001488 if args:
1489 files = ParseFiles(args, options.recursive)
agable0b65e732016-11-22 09:25:46 -08001490 change_scm = scm.determine_scm(options.root)
1491 if change_scm == 'git':
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001492 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001493 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001494 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001495 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001496 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001497 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001498 if not files:
1499 return None, None
1500 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001501 return change_class, files
1502
1503
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001504@contextlib.contextmanager
1505def canned_check_filter(method_names):
1506 filtered = {}
1507 try:
1508 for method_name in method_names:
1509 if not hasattr(presubmit_canned_checks, method_name):
Aaron Gableecee74c2018-04-02 15:13:08 -07001510 logging.warn('Skipping unknown "canned" check %s' % method_name)
1511 continue
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001512 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1513 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1514 yield
1515 finally:
1516 for name, method in filtered.iteritems():
1517 setattr(presubmit_canned_checks, name, method)
1518
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001519
Edward Lesmes1ad681e2018-04-03 17:15:57 -04001520def CallCommand(cmd_data):
1521 """Runs an external program, potentially from a child process created by the
1522 multiprocessing module.
1523
1524 multiprocessing needs a top level function with a single argument.
1525
1526 This function converts invocation of .py files and invocations of "python" to
1527 vpython invocations.
1528 """
1529 vpython = 'vpython.bat' if sys.platform == 'win32' else 'vpython'
1530
1531 cmd = cmd_data.cmd
1532 if cmd[0] == 'python':
1533 cmd = list(cmd)
1534 cmd[0] = vpython
1535 elif cmd[0].endswith('.py'):
1536 cmd = [vpython] + cmd
1537
1538 cmd_data.kwargs['stdout'] = subprocess.PIPE
1539 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1540 try:
1541 start = time.time()
1542 (out, _), code = subprocess.communicate(cmd, **cmd_data.kwargs)
1543 duration = time.time() - start
1544 except OSError as e:
1545 duration = time.time() - start
1546 return cmd_data.message(
1547 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1548 if code != 0:
1549 return cmd_data.message(
1550 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1551 if cmd_data.info:
1552 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
1553
1554
sbc@chromium.org013731e2015-02-26 18:28:43 +00001555def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001556 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001557 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001558 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001559 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001560 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1561 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001562 parser.add_option("-r", "--recursive", action="store_true",
1563 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001564 parser.add_option("-v", "--verbose", action="count", default=0,
1565 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001566 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001567 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001568 parser.add_option("--description", default='')
1569 parser.add_option("--issue", type='int', default=0)
1570 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001571 parser.add_option("--root", default=os.getcwd(),
1572 help="Search for PRESUBMIT.py up to this directory. "
1573 "If inherit-review-settings-ok is present in this "
1574 "directory, parent directories up to the root file "
1575 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001576 parser.add_option("--upstream",
1577 help="Git only: the base ref or upstream branch against "
1578 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001579 parser.add_option("--default_presubmit")
1580 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001581 parser.add_option("--skip_canned", action='append', default=[],
1582 help="A list of checks to skip which appear in "
1583 "presubmit_canned_checks. Can be provided multiple times "
1584 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001585 parser.add_option("--dry_run", action='store_true',
1586 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001587 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001588 parser.add_option("--gerrit_fetch", action='store_true',
1589 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001590
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001591 options, args = parser.parse_args(argv)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001592
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001593 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001594 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001595 elif options.verbose:
1596 logging.basicConfig(level=logging.INFO)
1597 else:
1598 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001599
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001600 change_class, files = load_files(options, args)
1601 if not change_class:
1602 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001603 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001604
Aaron Gable668c1d82018-04-03 10:19:16 -07001605 gerrit_obj = None
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001606 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001607 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001608 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1609 options.author = gerrit_obj.GetChangeOwner(options.issue)
1610 options.description = gerrit_obj.GetChangeDescription(options.issue,
1611 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001612 logging.info('Got author: "%s"', options.author)
1613 logging.info('Got description: """\n%s\n"""', options.description)
1614
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001615 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001616 with canned_check_filter(options.skip_canned):
1617 results = DoPresubmitChecks(
1618 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001619 options.description,
1620 options.root,
1621 files,
1622 options.issue,
1623 options.patchset,
1624 options.author,
1625 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001626 options.commit,
1627 options.verbose,
1628 sys.stdout,
1629 sys.stdin,
1630 options.default_presubmit,
1631 options.may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001632 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001633 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001634 return not results.should_continue()
1635 except PresubmitFailure, e:
1636 print >> sys.stderr, e
1637 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001638 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001639
1640
1641if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001642 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001643 try:
1644 sys.exit(main())
1645 except KeyboardInterrupt:
1646 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001647 sys.exit(2)