blob: ab3f3d70b5ffad6b6cad113bc1e1a4a7650c02b5 [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.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000044import auth
maruel@chromium.org35625c72011-03-23 17:34:02 +000045import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000046import gclient_utils
Aaron Gableb584c4f2017-04-26 16:28:08 -070047import git_footers
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000048import gerrit_util
dpranke@chromium.org2a009622011-03-01 02:43:31 +000049import owners
Jochen Eisinger76f5fc62017-04-07 16:27:46 +020050import owners_finder
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000051import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000052import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000053import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000054import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000055
56
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000057# Ask for feedback only once in program lifetime.
58_ASKED_FOR_FEEDBACK = False
59
60
maruel@chromium.org899e1c12011-04-07 17:03:18 +000061class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000062 pass
63
64
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000065class CommandData(object):
66 def __init__(self, name, cmd, kwargs, message):
67 self.name = name
68 self.cmd = cmd
69 self.kwargs = kwargs
70 self.message = message
71 self.info = None
72
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000073
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000074def normpath(path):
75 '''Version of os.path.normpath that also changes backward slashes to
76 forward slashes when not running on Windows.
77 '''
78 # This is safe to always do because the Windows version of os.path.normpath
79 # will replace forward slashes with backward slashes.
80 path = path.replace(os.sep, '/')
81 return os.path.normpath(path)
82
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000083
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000084def _RightHandSideLinesImpl(affected_files):
85 """Implements RightHandSideLines for InputApi and GclChange."""
86 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000087 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000088 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000089 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000090
91
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000092class PresubmitOutput(object):
93 def __init__(self, input_stream=None, output_stream=None):
94 self.input_stream = input_stream
95 self.output_stream = output_stream
96 self.reviewers = []
Daniel Cheng7227d212017-11-17 08:12:37 -080097 self.more_cc = []
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000098 self.written_output = []
99 self.error_count = 0
100
101 def prompt_yes_no(self, prompt_string):
102 self.write(prompt_string)
103 if self.input_stream:
104 response = self.input_stream.readline().strip().lower()
105 if response not in ('y', 'yes'):
106 self.fail()
107 else:
108 self.fail()
109
110 def fail(self):
111 self.error_count += 1
112
113 def should_continue(self):
114 return not self.error_count
115
116 def write(self, s):
117 self.written_output.append(s)
118 if self.output_stream:
119 self.output_stream.write(s)
120
121 def getvalue(self):
122 return ''.join(self.written_output)
123
124
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000125# Top level object so multiprocessing can pickle
126# Public access through OutputApi object.
127class _PresubmitResult(object):
128 """Base class for result objects."""
129 fatal = False
130 should_prompt = False
131
132 def __init__(self, message, items=None, long_text=''):
133 """
134 message: A short one-line message to indicate errors.
135 items: A list of short strings to indicate where errors occurred.
136 long_text: multi-line text output, e.g. from another tool
137 """
138 self._message = message
139 self._items = items or []
140 if items:
141 self._items = items
142 self._long_text = long_text.rstrip()
143
144 def handle(self, output):
145 output.write(self._message)
146 output.write('\n')
147 for index, item in enumerate(self._items):
148 output.write(' ')
149 # Write separately in case it's unicode.
150 output.write(str(item))
151 if index < len(self._items) - 1:
152 output.write(' \\')
153 output.write('\n')
154 if self._long_text:
155 output.write('\n***************\n')
156 # Write separately in case it's unicode.
157 output.write(self._long_text)
158 output.write('\n***************\n')
159 if self.fatal:
160 output.fail()
161
162
163# Top level object so multiprocessing can pickle
164# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000165class _PresubmitError(_PresubmitResult):
166 """A hard presubmit error."""
167 fatal = True
168
169
170# Top level object so multiprocessing can pickle
171# Public access through OutputApi object.
172class _PresubmitPromptWarning(_PresubmitResult):
173 """An warning that prompts the user if they want to continue."""
174 should_prompt = True
175
176
177# Top level object so multiprocessing can pickle
178# Public access through OutputApi object.
179class _PresubmitNotifyResult(_PresubmitResult):
180 """Just print something to the screen -- but it's not even a warning."""
181 pass
182
183
184# Top level object so multiprocessing can pickle
185# Public access through OutputApi object.
186class _MailTextResult(_PresubmitResult):
187 """A warning that should be included in the review request email."""
188 def __init__(self, *args, **kwargs):
189 super(_MailTextResult, self).__init__()
190 raise NotImplementedError()
191
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000192class GerritAccessor(object):
193 """Limited Gerrit functionality for canned presubmit checks to work.
194
195 To avoid excessive Gerrit calls, caches the results.
196 """
197
198 def __init__(self, host):
199 self.host = host
200 self.cache = {}
201
202 def _FetchChangeDetail(self, issue):
203 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100204 try:
205 return gerrit_util.GetChangeDetail(
206 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700207 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100208 except gerrit_util.GerritError as e:
209 if e.http_status == 404:
210 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
211 'no credentials to fetch issue details' % issue)
212 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000213
214 def GetChangeInfo(self, issue):
215 """Returns labels and all revisions (patchsets) for this issue.
216
217 The result is a dictionary according to Gerrit REST Api.
218 https://gerrit-review.googlesource.com/Documentation/rest-api.html
219
220 However, API isn't very clear what's inside, so see tests for example.
221 """
222 assert issue
223 cache_key = int(issue)
224 if cache_key not in self.cache:
225 self.cache[cache_key] = self._FetchChangeDetail(issue)
226 return self.cache[cache_key]
227
228 def GetChangeDescription(self, issue, patchset=None):
229 """If patchset is none, fetches current patchset."""
230 info = self.GetChangeInfo(issue)
231 # info is a reference to cache. We'll modify it here adding description to
232 # it to the right patchset, if it is not yet there.
233
234 # Find revision info for the patchset we want.
235 if patchset is not None:
236 for rev, rev_info in info['revisions'].iteritems():
237 if str(rev_info['_number']) == str(patchset):
238 break
239 else:
240 raise Exception('patchset %s doesn\'t exist in issue %s' % (
241 patchset, issue))
242 else:
243 rev = info['current_revision']
244 rev_info = info['revisions'][rev]
245
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100246 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000247
Mun Yong Jang603d01e2017-12-19 16:38:30 -0800248 def GetDestRef(self, issue):
249 ref = self.GetChangeInfo(issue)['branch']
250 if not ref.startswith('refs/'):
251 # NOTE: it is possible to create 'refs/x' branch,
252 # aka 'refs/heads/refs/x'. However, this is ill-advised.
253 ref = 'refs/heads/%s' % ref
254 return ref
255
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000256 def GetChangeOwner(self, issue):
257 return self.GetChangeInfo(issue)['owner']['email']
258
259 def GetChangeReviewers(self, issue, approving_only=True):
Aaron Gable8b478f02017-07-31 15:33:19 -0700260 changeinfo = self.GetChangeInfo(issue)
261 if approving_only:
262 labelinfo = changeinfo.get('labels', {}).get('Code-Review', {})
263 values = labelinfo.get('values', {}).keys()
264 try:
265 max_value = max(int(v) for v in values)
266 reviewers = [r for r in labelinfo.get('all', [])
267 if r.get('value', 0) == max_value]
268 except ValueError: # values is the empty list
269 reviewers = []
270 else:
271 reviewers = changeinfo.get('reviewers', {}).get('REVIEWER', [])
272 return [r.get('email') for r in reviewers]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000273
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000274
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000275class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000276 """An instance of OutputApi gets passed to presubmit scripts so that they
277 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000278 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000279 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000280 PresubmitError = _PresubmitError
281 PresubmitPromptWarning = _PresubmitPromptWarning
282 PresubmitNotifyResult = _PresubmitNotifyResult
283 MailTextResult = _MailTextResult
284
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000285 def __init__(self, is_committing):
286 self.is_committing = is_committing
Daniel Cheng7227d212017-11-17 08:12:37 -0800287 self.more_cc = []
288
289 def AppendCC(self, cc):
290 """Appends a user to cc for this change."""
291 self.more_cc.append(cc)
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000292
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000293 def PresubmitPromptOrNotify(self, *args, **kwargs):
294 """Warn the user when uploading, but only notify if committing."""
295 if self.is_committing:
296 return self.PresubmitNotifyResult(*args, **kwargs)
297 return self.PresubmitPromptWarning(*args, **kwargs)
298
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800299 def EnsureCQIncludeTrybotsAreAdded(self, cl, bots_to_include, message):
300 """Helper for any PostUploadHook wishing to add CQ_INCLUDE_TRYBOTS.
301
302 Merges the bots_to_include into the current CQ_INCLUDE_TRYBOTS list,
303 keeping it alphabetically sorted. Returns the results that should be
304 returned from the PostUploadHook.
305
306 Args:
307 cl: The git_cl.Changelist object.
308 bots_to_include: A list of strings of bots to include, in the form
309 "master:slave".
310 message: A message to be printed in the case that
311 CQ_INCLUDE_TRYBOTS was updated.
312 """
313 description = cl.GetDescription(force=True)
Aaron Gableb584c4f2017-04-26 16:28:08 -0700314 include_re = re.compile(r'^CQ_INCLUDE_TRYBOTS=(.*)$', re.M | re.I)
315
316 prior_bots = []
317 if cl.IsGerrit():
318 trybot_footers = git_footers.parse_footers(description).get(
319 git_footers.normalize_name('Cq-Include-Trybots'), [])
320 for f in trybot_footers:
321 prior_bots += [b.strip() for b in f.split(';') if b.strip()]
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800322 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700323 trybot_tags = include_re.finditer(description)
324 for t in trybot_tags:
325 prior_bots += [b.strip() for b in t.group(1).split(';') if b.strip()]
326
327 if set(prior_bots) >= set(bots_to_include):
328 return []
329 all_bots = ';'.join(sorted(set(prior_bots) | set(bots_to_include)))
330
331 if cl.IsGerrit():
332 description = git_footers.remove_footer(
333 description, 'Cq-Include-Trybots')
334 description = git_footers.add_footer(
335 description, 'Cq-Include-Trybots', all_bots,
336 before_keys=['Change-Id'])
337 else:
338 new_include_trybots = 'CQ_INCLUDE_TRYBOTS=%s' % all_bots
339 m = include_re.search(description)
340 if m:
341 description = include_re.sub(new_include_trybots, description)
Kenneth Russelldf6e7342017-04-24 17:07:41 -0700342 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700343 description = '%s\n%s\n' % (description, new_include_trybots)
344
345 cl.UpdateDescription(description, force=True)
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800346 return [self.PresubmitNotifyResult(message)]
347
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000348
349class InputApi(object):
350 """An instance of this object is passed to presubmit scripts so they can
351 know stuff about the change they're looking at.
352 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000353 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800354 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000355
maruel@chromium.org3410d912009-06-09 20:56:16 +0000356 # File extensions that are considered source files from a style guide
357 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000358 #
359 # Files without an extension aren't included in the list. If you want to
360 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
361 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000362 DEFAULT_WHITE_LIST = (
363 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000364 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
365 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000366 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000367 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000368 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000369 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000370 )
371
372 # Path regexp that should be excluded from being considered containing source
373 # files. Don't modify this list from a presubmit script!
374 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000375 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000376 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000377 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
378 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000379 # Output directories (just in case)
380 r".*\bDebug[\\\/].*",
381 r".*\bRelease[\\\/].*",
382 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000383 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000384 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000385 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000386 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000387 r"(|.*[\\\/])\.git[\\\/].*",
388 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000389 # There is no point in processing a patch file.
390 r".+\.diff$",
391 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000392 )
393
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000394 def __init__(self, change, presubmit_path, is_committing,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000395 rietveld_obj, verbose, gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000396 """Builds an InputApi object.
397
398 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000399 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000400 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000401 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000402 rietveld_obj: rietveld.Rietveld client object
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000403 gerrit_obj: provides basic Gerrit codereview functionality.
404 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000405 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000406 # Version number of the presubmit_support script.
407 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000408 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000409 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000410 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000411 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000412 self.dry_run = dry_run
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000413 # TBD
414 self.host_url = 'http://codereview.chromium.org'
415 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000416 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000417
418 # We expose various modules and functions as attributes of the input_api
419 # so that presubmit scripts don't have to import them.
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +0900420 self.ast = ast
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000421 self.basename = os.path.basename
422 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000423 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000424 self.cStringIO = cStringIO
dcheng091b7db2016-06-16 01:27:51 -0700425 self.fnmatch = fnmatch
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000426 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000427 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000428 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000429 self.os_listdir = os.listdir
430 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000431 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000432 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000433 self.pickle = pickle
434 self.marshal = marshal
435 self.re = re
436 self.subprocess = subprocess
437 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000438 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000439 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000440 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000441 self.urllib2 = urllib2
442
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000443 # To easily fork python.
444 self.python_executable = sys.executable
445 self.environ = os.environ
446
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000447 # InputApi.platform is the platform you're currently running on.
448 self.platform = sys.platform
449
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000450 self.cpu_count = multiprocessing.cpu_count()
451
Aaron Gable5254da12017-09-06 21:05:39 +0000452 # this is done here because in RunTests, the current working directory has
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000453 # changed, which causes Pool() to explode fantastically when run on windows
454 # (because it tries to load the __main__ module, which imports lots of
455 # things relative to the current working directory).
Aaron Gable5254da12017-09-06 21:05:39 +0000456 self._run_tests_pool = multiprocessing.Pool(self.cpu_count)
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000457
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000458 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000459 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000460
461 # We carry the canned checks so presubmit scripts can easily use them.
462 self.canned_checks = presubmit_canned_checks
463
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100464 # Temporary files we must manually remove at the end of a run.
465 self._named_temporary_files = []
Jochen Eisinger72606f82017-04-04 10:44:18 +0200466
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000467 # TODO(dpranke): figure out a list of all approved owners for a repo
468 # in order to be able to handle wildcard OWNERS files?
469 self.owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +0200470 fopen=file, os_path=self.os_path)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200471 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000472 self.verbose = verbose
Dan Jacques94652a32017-10-09 23:18:46 -0400473 self.is_windows = sys.platform == 'win32'
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000474 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000475
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000476 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000477 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000478 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800479 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000480 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000481 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000482 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
483 for (a, b, header) in cpplint._re_pattern_templates
484 ]
485
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000486 def PresubmitLocalPath(self):
487 """Returns the local path of the presubmit script currently being run.
488
489 This is useful if you don't want to hard-code absolute paths in the
490 presubmit script. For example, It can be used to find another file
491 relative to the PRESUBMIT.py script, so the whole tree can be branched and
492 the presubmit script still works, without editing its content.
493 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000494 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000495
agable0b65e732016-11-22 09:25:46 -0800496 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000497 """Same as input_api.change.AffectedFiles() except only lists files
498 (and optionally directories) in the same directory as the current presubmit
499 script, or subdirectories thereof.
500 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000501 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000502 if len(dir_with_slash) == 1:
503 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000504
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000505 return filter(
506 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
agable0b65e732016-11-22 09:25:46 -0800507 self.change.AffectedFiles(include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000508
agable0b65e732016-11-22 09:25:46 -0800509 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000510 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800511 paths = [af.LocalPath() for af in self.AffectedFiles()]
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000512 logging.debug("LocalPaths: %s", paths)
513 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000514
agable0b65e732016-11-22 09:25:46 -0800515 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000516 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800517 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000518
agable0b65e732016-11-22 09:25:46 -0800519 def AffectedTestableFiles(self, include_deletes=None):
520 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000521 in the same directory as the current presubmit script, or subdirectories
522 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000523 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000524 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800525 warn("AffectedTestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000526 " is deprecated and ignored" % str(include_deletes),
527 category=DeprecationWarning,
528 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800529 return filter(lambda x: x.IsTestableFile(),
530 self.AffectedFiles(include_deletes=False))
531
532 def AffectedTextFiles(self, include_deletes=None):
533 """An alias to AffectedTestableFiles for backwards compatibility."""
534 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000535
maruel@chromium.org3410d912009-06-09 20:56:16 +0000536 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
537 """Filters out files that aren't considered "source file".
538
539 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
540 and InputApi.DEFAULT_BLACK_LIST is used respectively.
541
542 The lists will be compiled as regular expression and
543 AffectedFile.LocalPath() needs to pass both list.
544
545 Note: Copy-paste this function to suit your needs or use a lambda function.
546 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000547 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000548 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000549 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000550 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000551 return True
552 return False
553 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
554 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
555
556 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800557 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000558
559 If source_file is None, InputApi.FilterSourceFile() is used.
560 """
561 if not source_file:
562 source_file = self.FilterSourceFile
agable0b65e732016-11-22 09:25:46 -0800563 return filter(source_file, self.AffectedTestableFiles())
maruel@chromium.org3410d912009-06-09 20:56:16 +0000564
565 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000566 """An iterator over all text lines in "new" version of changed files.
567
568 Only lists lines from new or modified text files in the change that are
569 contained by the directory of the currently executing presubmit script.
570
571 This is useful for doing line-by-line regex checks, like checking for
572 trailing whitespace.
573
574 Yields:
575 a 3 tuple:
576 the AffectedFile instance of the current file;
577 integer line number (1-based); and
578 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000579
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000580 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000581 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000582 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000583 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000584
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000585 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000586 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000587
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000588 Deny reading anything outside the repository.
589 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000590 if isinstance(file_item, AffectedFile):
591 file_item = file_item.AbsoluteLocalPath()
592 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000593 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000594 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000595
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100596 def CreateTemporaryFile(self, **kwargs):
597 """Returns a named temporary file that must be removed with a call to
598 RemoveTemporaryFiles().
599
600 All keyword arguments are forwarded to tempfile.NamedTemporaryFile(),
601 except for |delete|, which is always set to False.
602
603 Presubmit checks that need to create a temporary file and pass it for
604 reading should use this function instead of NamedTemporaryFile(), as
605 Windows fails to open a file that is already open for writing.
606
607 with input_api.CreateTemporaryFile() as f:
608 f.write('xyz')
609 f.close()
610 input_api.subprocess.check_output(['script-that', '--reads-from',
611 f.name])
612
613
614 Note that callers of CreateTemporaryFile() should not worry about removing
615 any temporary file; this is done transparently by the presubmit handling
616 code.
617 """
618 if 'delete' in kwargs:
619 # Prevent users from passing |delete|; we take care of file deletion
620 # ourselves and this prevents unintuitive error messages when we pass
621 # delete=False and 'delete' is also in kwargs.
622 raise TypeError('CreateTemporaryFile() does not take a "delete" '
623 'argument, file deletion is handled automatically by '
624 'the same presubmit_support code that creates InputApi '
625 'objects.')
626 temp_file = self.tempfile.NamedTemporaryFile(delete=False, **kwargs)
627 self._named_temporary_files.append(temp_file.name)
628 return temp_file
629
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000630 @property
631 def tbr(self):
632 """Returns if a change is TBR'ed."""
Jeremy Romandce22502017-06-20 15:37:29 -0400633 return 'TBR' in self.change.tags or self.change.TBRsFromDescription()
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000634
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000635 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000636 tests = []
637 msgs = []
638 for t in tests_mix:
639 if isinstance(t, OutputApi.PresubmitResult):
640 msgs.append(t)
641 else:
642 assert issubclass(t.message, _PresubmitResult)
643 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000644 if self.verbose:
645 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000646 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000647 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000648 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000649 else:
650 msgs.extend(map(CallCommand, tests))
651 return [m for m in msgs if m]
652
scottmg86099d72016-09-01 09:16:51 -0700653 def ShutdownPool(self):
654 self._run_tests_pool.close()
655 self._run_tests_pool.join()
656 self._run_tests_pool = None
657
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000658
nick@chromium.orgff526192013-06-10 19:30:26 +0000659class _DiffCache(object):
660 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000661 def __init__(self, upstream=None):
662 """Stores the upstream revision against which all diffs will be computed."""
663 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000664
665 def GetDiff(self, path, local_root):
666 """Get the diff for a particular path."""
667 raise NotImplementedError()
668
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700669 def GetOldContents(self, path, local_root):
670 """Get the old version for a particular path."""
671 raise NotImplementedError()
672
nick@chromium.orgff526192013-06-10 19:30:26 +0000673
nick@chromium.orgff526192013-06-10 19:30:26 +0000674class _GitDiffCache(_DiffCache):
675 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000676 def __init__(self, upstream):
677 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000678 self._diffs_by_file = None
679
680 def GetDiff(self, path, local_root):
681 if not self._diffs_by_file:
682 # Compute a single diff for all files and parse the output; should
683 # with git this is much faster than computing one diff for each file.
684 diffs = {}
685
686 # Don't specify any filenames below, because there are command line length
687 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000688 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
689 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000690
691 # This regex matches the path twice, separated by a space. Note that
692 # filename itself may contain spaces.
693 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
694 current_diff = []
695 keep_line_endings = True
696 for x in unified_diff.splitlines(keep_line_endings):
697 match = file_marker.match(x)
698 if match:
699 # Marks the start of a new per-file section.
700 diffs[match.group('filename')] = current_diff = [x]
701 elif x.startswith('diff --git'):
702 raise PresubmitFailure('Unexpected diff line: %s' % x)
703 else:
704 current_diff.append(x)
705
706 self._diffs_by_file = dict(
707 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
708
709 if path not in self._diffs_by_file:
710 raise PresubmitFailure(
711 'Unified diff did not contain entry for file %s' % path)
712
713 return self._diffs_by_file[path]
714
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700715 def GetOldContents(self, path, local_root):
716 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
717
nick@chromium.orgff526192013-06-10 19:30:26 +0000718
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000719class AffectedFile(object):
720 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000721
722 DIFF_CACHE = _DiffCache
723
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000724 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800725 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000726 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000727 self._path = path
728 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000729 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000730 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000731 self._cached_changed_contents = None
732 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000733 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700734 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000735
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000736 def LocalPath(self):
737 """Returns the path of this file on the local disk relative to client root.
Andrew Grieve92b8b992017-11-02 09:42:24 -0400738
739 This should be used for error messages but not for accessing files,
740 because presubmit checks are run with CWD=PresubmitLocalPath() (which is
741 often != client root).
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000743 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000744
745 def AbsoluteLocalPath(self):
746 """Returns the absolute path of this file on the local disk.
747 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000748 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000749
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000750 def Action(self):
751 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000752 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000753
agable0b65e732016-11-22 09:25:46 -0800754 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000755 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000756
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000757 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000758 raise NotImplementedError() # Implement when needed
759
agable0b65e732016-11-22 09:25:46 -0800760 def IsTextFile(self):
761 """An alias to IsTestableFile for backwards compatibility."""
762 return self.IsTestableFile()
763
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700764 def OldContents(self):
765 """Returns an iterator over the lines in the old version of file.
766
Daniel Cheng2da34fe2017-03-21 20:42:12 -0700767 The old version is the file before any modifications in the user's
768 workspace, i.e. the "left hand side".
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700769
770 Contents will be empty if the file is a directory or does not exist.
771 Note: The carriage returns (LF or CR) are stripped off.
772 """
773 return self._diff_cache.GetOldContents(self.LocalPath(),
774 self._local_root).splitlines()
775
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000776 def NewContents(self):
777 """Returns an iterator over the lines in the new version of file.
778
779 The new version is the file in the user's workspace, i.e. the "right hand
780 side".
781
782 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000783 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000784 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000785 if self._cached_new_contents is None:
786 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800787 try:
788 self._cached_new_contents = gclient_utils.FileRead(
789 self.AbsoluteLocalPath(), 'rU').splitlines()
790 except IOError:
791 pass # File not found? That's fine; maybe it was deleted.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000792 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000793
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000794 def ChangedContents(self):
795 """Returns a list of tuples (line number, line text) of all new lines.
796
797 This relies on the scm diff output describing each changed code section
798 with a line of the form
799
800 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
801 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000802 if self._cached_changed_contents is not None:
803 return self._cached_changed_contents[:]
804 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000805 line_num = 0
806
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000807 for line in self.GenerateScmDiff().splitlines():
808 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
809 if m:
810 line_num = int(m.groups(1)[0])
811 continue
812 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000813 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000814 if not line.startswith('-'):
815 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000816 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000817
maruel@chromium.org5de13972009-06-10 18:16:06 +0000818 def __str__(self):
819 return self.LocalPath()
820
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000821 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000822 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000823
maruel@chromium.org58407af2011-04-12 23:15:57 +0000824
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000825class GitAffectedFile(AffectedFile):
826 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000827 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800828 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000829
nick@chromium.orgff526192013-06-10 19:30:26 +0000830 DIFF_CACHE = _GitDiffCache
831
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000832 def __init__(self, *args, **kwargs):
833 AffectedFile.__init__(self, *args, **kwargs)
834 self._server_path = None
agable0b65e732016-11-22 09:25:46 -0800835 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000836
agable0b65e732016-11-22 09:25:46 -0800837 def IsTestableFile(self):
838 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000839 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -0800840 # A deleted file is not testable.
841 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000842 else:
agable0b65e732016-11-22 09:25:46 -0800843 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
844 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000845
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000846
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000847class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000848 """Describe a change.
849
850 Used directly by the presubmit scripts to query the current change being
851 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000852
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000853 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000854 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000855 self.KEY: equivalent to tags['KEY']
856 """
857
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000858 _AFFECTED_FILES = AffectedFile
859
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000860 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000861 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000862 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000863 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000864
maruel@chromium.org58407af2011-04-12 23:15:57 +0000865 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000866 self, name, description, local_root, files, issue, patchset, author,
867 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000868 if files is None:
869 files = []
870 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000871 # Convert root into an absolute path.
872 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000873 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000874 self.issue = issue
875 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000876 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000877
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000878 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000879 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000880 self._description_without_tags = ''
881 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000882
maruel@chromium.orge085d812011-10-10 19:49:15 +0000883 assert all(
884 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
885
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000886 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000887 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000888 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
889 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000890 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000891
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000892 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000893 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000894 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000895
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000896 def DescriptionText(self):
897 """Returns the user-entered changelist description, minus tags.
898
899 Any line in the user-provided description starting with e.g. "FOO="
900 (whitespace permitted before and around) is considered a tag line. Such
901 lines are stripped out of the description this function returns.
902 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000903 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000904
905 def FullDescriptionText(self):
906 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000907 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000908
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000909 def SetDescriptionText(self, description):
910 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000911
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000912 Also updates the list of tags."""
913 self._full_description = description
914
915 # From the description text, build up a dictionary of key/value pairs
916 # plus the description minus all key/value or "tag" lines.
917 description_without_tags = []
918 self.tags = {}
919 for line in self._full_description.splitlines():
920 m = self.TAG_LINE_RE.match(line)
921 if m:
922 self.tags[m.group('key')] = m.group('value')
923 else:
924 description_without_tags.append(line)
925
926 # Change back to text and remove whitespace at end.
927 self._description_without_tags = (
928 '\n'.join(description_without_tags).rstrip())
929
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000930 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000931 """Returns the repository (checkout) root directory for this change,
932 as an absolute path.
933 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000934 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000935
936 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000937 """Return tags directly as attributes on the object."""
938 if not re.match(r"^[A-Z_]*$", attr):
939 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000940 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000941
Aaron Gablefc03e672017-05-15 14:09:42 -0700942 def BugsFromDescription(self):
943 """Returns all bugs referenced in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700944 tags = [b.strip() for b in self.tags.get('BUG', '').split(',') if b.strip()]
945 footers = git_footers.parse_footers(self._full_description).get('Bug', [])
946 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -0700947
948 def ReviewersFromDescription(self):
949 """Returns all reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700950 # We don't support a "R:" git-footer for reviewers; that is in metadata.
951 tags = [r.strip() for r in self.tags.get('R', '').split(',') if r.strip()]
952 return sorted(set(tags))
Aaron Gablefc03e672017-05-15 14:09:42 -0700953
954 def TBRsFromDescription(self):
955 """Returns all TBR reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700956 tags = [r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()]
957 # TODO(agable): Remove support for 'Tbr:' when TBRs are programmatically
958 # determined by self-CR+1s.
959 footers = git_footers.parse_footers(self._full_description).get('Tbr', [])
960 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -0700961
962 # TODO(agable): Delete these once we're sure they're unused.
963 @property
964 def BUG(self):
965 return ','.join(self.BugsFromDescription())
966 @property
967 def R(self):
968 return ','.join(self.ReviewersFromDescription())
969 @property
970 def TBR(self):
971 return ','.join(self.TBRsFromDescription())
972
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000973 def AllFiles(self, root=None):
974 """List all files under source control in the repo."""
975 raise NotImplementedError()
976
agable0b65e732016-11-22 09:25:46 -0800977 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000978 """Returns a list of AffectedFile instances for all files in the change.
979
980 Args:
981 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +0000982 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000983
984 Returns:
985 [AffectedFile(path, action), AffectedFile(path, action)]
986 """
agable0b65e732016-11-22 09:25:46 -0800987 affected = filter(file_filter, self._affected_files)
sail@chromium.org5538e022011-05-12 17:53:16 +0000988
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000989 if include_deletes:
990 return affected
Lei Zhang9611c4c2017-04-04 01:41:56 -0700991 return filter(lambda x: x.Action() != 'D', affected)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000992
agable0b65e732016-11-22 09:25:46 -0800993 def AffectedTestableFiles(self, include_deletes=None):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000994 """Return a list of the existing text files in a change."""
995 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800996 warn("AffectedTeestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000997 " is deprecated and ignored" % str(include_deletes),
998 category=DeprecationWarning,
999 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -08001000 return filter(lambda x: x.IsTestableFile(),
1001 self.AffectedFiles(include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001002
agable0b65e732016-11-22 09:25:46 -08001003 def AffectedTextFiles(self, include_deletes=None):
1004 """An alias to AffectedTestableFiles for backwards compatibility."""
1005 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006
agable0b65e732016-11-22 09:25:46 -08001007 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001009 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010
agable0b65e732016-11-22 09:25:46 -08001011 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001012 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001013 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001014
1015 def RightHandSideLines(self):
1016 """An iterator over all text lines in "new" version of changed files.
1017
1018 Lists lines from new or modified text files in the change.
1019
1020 This is useful for doing line-by-line regex checks, like checking for
1021 trailing whitespace.
1022
1023 Yields:
1024 a 3 tuple:
1025 the AffectedFile instance of the current file;
1026 integer line number (1-based); and
1027 the contents of the line as a string.
1028 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001029 return _RightHandSideLinesImpl(
1030 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -08001031 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001032
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02001033 def OriginalOwnersFiles(self):
1034 """A map from path names of affected OWNERS files to their old content."""
1035 def owners_file_filter(f):
1036 return 'OWNERS' in os.path.split(f.LocalPath())[1]
1037 files = self.AffectedFiles(file_filter=owners_file_filter)
1038 return dict([(f.LocalPath(), f.OldContents()) for f in files])
1039
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001040
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001041class GitChange(Change):
1042 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001043 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001044
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001045 def AllFiles(self, root=None):
1046 """List all files under source control in the repo."""
1047 root = root or self.RepositoryRoot()
1048 return subprocess.check_output(
Aaron Gable7817f022017-12-12 09:43:17 -08001049 ['git', '-c', 'core.quotePath=false', 'ls-files', '--', '.'],
1050 cwd=root).splitlines()
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001051
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001052
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001053def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001054 """Finds all presubmit files that apply to a given set of source files.
1055
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001056 If inherit-review-settings-ok is present right under root, looks for
1057 PRESUBMIT.py in directories enclosing root.
1058
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001059 Args:
1060 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001061 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001062
1063 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001064 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001065 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001066 files = [normpath(os.path.join(root, f)) for f in files]
1067
1068 # List all the individual directories containing files.
1069 directories = set([os.path.dirname(f) for f in files])
1070
1071 # Ignore root if inherit-review-settings-ok is present.
1072 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1073 root = None
1074
1075 # Collect all unique directories that may contain PRESUBMIT.py.
1076 candidates = set()
1077 for directory in directories:
1078 while True:
1079 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001080 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001081 candidates.add(directory)
1082 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001083 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001084 parent_dir = os.path.dirname(directory)
1085 if parent_dir == directory:
1086 # We hit the system root directory.
1087 break
1088 directory = parent_dir
1089
1090 # Look for PRESUBMIT.py in all candidate directories.
1091 results = []
1092 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -07001093 try:
1094 for f in os.listdir(directory):
1095 p = os.path.join(directory, f)
1096 if os.path.isfile(p) and re.match(
1097 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1098 results.append(p)
1099 except OSError:
1100 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001101
tobiasjs2836bcf2016-08-16 04:08:16 -07001102 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001103 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001104
1105
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001106class GetTryMastersExecuter(object):
1107 @staticmethod
1108 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1109 """Executes GetPreferredTryMasters() from a single presubmit script.
1110
1111 Args:
1112 script_text: The text of the presubmit script.
1113 presubmit_path: Project script to run.
1114 project: Project name to pass to presubmit script for bot selection.
1115
1116 Return:
1117 A map of try masters to map of builders to set of tests.
1118 """
1119 context = {}
1120 try:
1121 exec script_text in context
1122 except Exception, e:
1123 raise PresubmitFailure('"%s" had an exception.\n%s'
1124 % (presubmit_path, e))
1125
1126 function_name = 'GetPreferredTryMasters'
1127 if function_name not in context:
1128 return {}
1129 get_preferred_try_masters = context[function_name]
1130 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1131 raise PresubmitFailure(
1132 'Expected function "GetPreferredTryMasters" to take two arguments.')
1133 return get_preferred_try_masters(project, change)
1134
1135
rmistry@google.com5626a922015-02-26 14:03:30 +00001136class GetPostUploadExecuter(object):
1137 @staticmethod
1138 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1139 """Executes PostUploadHook() from a single presubmit script.
1140
1141 Args:
1142 script_text: The text of the presubmit script.
1143 presubmit_path: Project script to run.
1144 cl: The Changelist object.
1145 change: The Change object.
1146
1147 Return:
1148 A list of results objects.
1149 """
1150 context = {}
1151 try:
1152 exec script_text in context
1153 except Exception, e:
1154 raise PresubmitFailure('"%s" had an exception.\n%s'
1155 % (presubmit_path, e))
1156
1157 function_name = 'PostUploadHook'
1158 if function_name not in context:
1159 return {}
1160 post_upload_hook = context[function_name]
1161 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1162 raise PresubmitFailure(
1163 'Expected function "PostUploadHook" to take three arguments.')
1164 return post_upload_hook(cl, change, OutputApi(False))
1165
1166
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001167def _MergeMasters(masters1, masters2):
1168 """Merges two master maps. Merges also the tests of each builder."""
1169 result = {}
1170 for (master, builders) in itertools.chain(masters1.iteritems(),
1171 masters2.iteritems()):
1172 new_builders = result.setdefault(master, {})
1173 for (builder, tests) in builders.iteritems():
1174 new_builders.setdefault(builder, set([])).update(tests)
1175 return result
1176
1177
1178def DoGetTryMasters(change,
1179 changed_files,
1180 repository_root,
1181 default_presubmit,
1182 project,
1183 verbose,
1184 output_stream):
1185 """Get the list of try masters from the presubmit scripts.
1186
1187 Args:
1188 changed_files: List of modified files.
1189 repository_root: The repository root.
1190 default_presubmit: A default presubmit script to execute in any case.
1191 project: Optional name of a project used in selecting trybots.
1192 verbose: Prints debug info.
1193 output_stream: A stream to write debug output to.
1194
1195 Return:
1196 Map of try masters to map of builders to set of tests.
1197 """
1198 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1199 if not presubmit_files and verbose:
1200 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1201 results = {}
1202 executer = GetTryMastersExecuter()
1203
1204 if default_presubmit:
1205 if verbose:
1206 output_stream.write("Running default presubmit script.\n")
1207 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1208 results = _MergeMasters(results, executer.ExecPresubmitScript(
1209 default_presubmit, fake_path, project, change))
1210 for filename in presubmit_files:
1211 filename = os.path.abspath(filename)
1212 if verbose:
1213 output_stream.write("Running %s\n" % filename)
1214 # Accept CRLF presubmit script.
1215 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1216 results = _MergeMasters(results, executer.ExecPresubmitScript(
1217 presubmit_script, filename, project, change))
1218
1219 # Make sets to lists again for later JSON serialization.
1220 for builders in results.itervalues():
1221 for builder in builders:
1222 builders[builder] = list(builders[builder])
1223
1224 if results and verbose:
1225 output_stream.write('%s\n' % str(results))
1226 return results
1227
1228
rmistry@google.com5626a922015-02-26 14:03:30 +00001229def DoPostUploadExecuter(change,
1230 cl,
1231 repository_root,
1232 verbose,
1233 output_stream):
1234 """Execute the post upload hook.
1235
1236 Args:
1237 change: The Change object.
1238 cl: The Changelist object.
1239 repository_root: The repository root.
1240 verbose: Prints debug info.
1241 output_stream: A stream to write debug output to.
1242 """
1243 presubmit_files = ListRelevantPresubmitFiles(
1244 change.LocalPaths(), repository_root)
1245 if not presubmit_files and verbose:
1246 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1247 results = []
1248 executer = GetPostUploadExecuter()
1249 # The root presubmit file should be executed after the ones in subdirectories.
1250 # i.e. the specific post upload hooks should run before the general ones.
1251 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1252 presubmit_files.reverse()
1253
1254 for filename in presubmit_files:
1255 filename = os.path.abspath(filename)
1256 if verbose:
1257 output_stream.write("Running %s\n" % filename)
1258 # Accept CRLF presubmit script.
1259 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1260 results.extend(executer.ExecPresubmitScript(
1261 presubmit_script, filename, cl, change))
1262 output_stream.write('\n')
1263 if results:
1264 output_stream.write('** Post Upload Hook Messages **\n')
1265 for result in results:
1266 result.handle(output_stream)
1267 output_stream.write('\n')
1268
1269 return results
1270
1271
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001272class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001273 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001274 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001275 """
1276 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001277 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001278 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001279 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001280 gerrit_obj: provides basic Gerrit codereview functionality.
1281 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001282 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001283 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001284 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001285 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001286 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001287 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001288 self.dry_run = dry_run
Daniel Cheng7227d212017-11-17 08:12:37 -08001289 self.more_cc = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001290
1291 def ExecPresubmitScript(self, script_text, presubmit_path):
1292 """Executes a single presubmit script.
1293
1294 Args:
1295 script_text: The text of the presubmit script.
1296 presubmit_path: The path to the presubmit file (this will be reported via
1297 input_api.PresubmitLocalPath()).
1298
1299 Return:
1300 A list of result objects, empty if no problems.
1301 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001302
chase@chromium.org8e416c82009-10-06 04:30:44 +00001303 # Change to the presubmit file's directory to support local imports.
1304 main_path = os.getcwd()
1305 os.chdir(os.path.dirname(presubmit_path))
1306
1307 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001308 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001309 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001310 gerrit_obj=self.gerrit, dry_run=self.dry_run)
Daniel Cheng7227d212017-11-17 08:12:37 -08001311 output_api = OutputApi(self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001312 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001313 try:
1314 exec script_text in context
1315 except Exception, e:
1316 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001317
1318 # These function names must change if we make substantial changes to
1319 # the presubmit API that are not backwards compatible.
1320 if self.committing:
1321 function_name = 'CheckChangeOnCommit'
1322 else:
1323 function_name = 'CheckChangeOnUpload'
1324 if function_name in context:
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +01001325 try:
Daniel Cheng7227d212017-11-17 08:12:37 -08001326 context['__args'] = (input_api, output_api)
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +01001327 logging.debug('Running %s in %s', function_name, presubmit_path)
1328 result = eval(function_name + '(*__args)', context)
1329 logging.debug('Running %s done.', function_name)
Daniel Chengd36fce42017-11-21 21:52:52 -08001330 self.more_cc.extend(output_api.more_cc)
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +01001331 finally:
1332 map(os.remove, input_api._named_temporary_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001333 if not (isinstance(result, types.TupleType) or
1334 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001335 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001336 'Presubmit functions must return a tuple or list')
1337 for item in result:
1338 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001339 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001340 'All presubmit results must be of types derived from '
1341 'output_api.PresubmitResult')
1342 else:
1343 result = () # no error since the script doesn't care about current event.
1344
scottmg86099d72016-09-01 09:16:51 -07001345 input_api.ShutdownPool()
1346
chase@chromium.org8e416c82009-10-06 04:30:44 +00001347 # Return the process to the original working directory.
1348 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001349 return result
1350
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001351def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001352 committing,
1353 verbose,
1354 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001355 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001356 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001357 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001358 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001359 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001360 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001361 """Runs all presubmit checks that apply to the files in the change.
1362
1363 This finds all PRESUBMIT.py files in directories enclosing the files in the
1364 change (up to the repository root) and calls the relevant entrypoint function
1365 depending on whether the change is being committed or uploaded.
1366
1367 Prints errors, warnings and notifications. Prompts the user for warnings
1368 when needed.
1369
1370 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001371 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001372 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001373 verbose: Prints debug info.
1374 output_stream: A stream to write output from presubmit tests to.
1375 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001376 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001377 may_prompt: Enable (y/n) questions on warning or error. If False,
1378 any questions are answered with yes by default.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001379 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001380 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001381 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001382
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001383 Warning:
1384 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1385 SHOULD be sys.stdin.
1386
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001387 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001388 A PresubmitOutput object. Use output.should_continue() to figure out
1389 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001390 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001391 old_environ = os.environ
1392 try:
1393 # Make sure python subprocesses won't generate .pyc files.
1394 os.environ = os.environ.copy()
1395 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001396
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001397 output = PresubmitOutput(input_stream, output_stream)
1398 if committing:
1399 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001400 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001401 output.write("Running presubmit upload checks ...\n")
1402 start_time = time.time()
1403 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001404 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001405 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001406 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001407 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001408 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001409 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001410 if default_presubmit:
1411 if verbose:
1412 output.write("Running default presubmit script.\n")
1413 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1414 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1415 for filename in presubmit_files:
1416 filename = os.path.abspath(filename)
1417 if verbose:
1418 output.write("Running %s\n" % filename)
1419 # Accept CRLF presubmit script.
1420 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1421 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001422
Daniel Cheng7227d212017-11-17 08:12:37 -08001423 output.more_cc.extend(executer.more_cc)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001424 errors = []
1425 notifications = []
1426 warnings = []
1427 for result in results:
1428 if result.fatal:
1429 errors.append(result)
1430 elif result.should_prompt:
1431 warnings.append(result)
1432 else:
1433 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001434
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001435 output.write('\n')
1436 for name, items in (('Messages', notifications),
1437 ('Warnings', warnings),
1438 ('ERRORS', errors)):
1439 if items:
1440 output.write('** Presubmit %s **\n' % name)
1441 for item in items:
1442 item.handle(output)
1443 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001444
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001445 total_time = time.time() - start_time
1446 if total_time > 1.0:
1447 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001448
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001449 if errors:
1450 output.fail()
1451 elif warnings:
1452 output.write('There were presubmit warnings. ')
1453 if may_prompt:
1454 output.prompt_yes_no('Are you sure you wish to continue? (y/N): ')
1455 else:
1456 output.write('Presubmit checks passed.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001457
1458 global _ASKED_FOR_FEEDBACK
1459 # Ask for feedback one time out of 5.
1460 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001461 output.write(
1462 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1463 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1464 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001465 _ASKED_FOR_FEEDBACK = True
1466 return output
1467 finally:
1468 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001469
1470
1471def ScanSubDirs(mask, recursive):
1472 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001473 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001474
1475 results = []
1476 for root, dirs, files in os.walk('.'):
1477 if '.svn' in dirs:
1478 dirs.remove('.svn')
1479 if '.git' in dirs:
1480 dirs.remove('.git')
1481 for name in files:
1482 if fnmatch.fnmatch(name, mask):
1483 results.append(os.path.join(root, name))
1484 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001485
1486
1487def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001488 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001489 files = []
1490 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001491 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001492 return files
1493
1494
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001495def load_files(options, args):
1496 """Tries to determine the SCM."""
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001497 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001498 if args:
1499 files = ParseFiles(args, options.recursive)
agable0b65e732016-11-22 09:25:46 -08001500 change_scm = scm.determine_scm(options.root)
1501 if change_scm == 'git':
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001502 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001503 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001504 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001505 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001506 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001507 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001508 if not files:
1509 return None, None
1510 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001511 return change_class, files
1512
1513
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001514class NonexistantCannedCheckFilter(Exception):
1515 pass
1516
1517
1518@contextlib.contextmanager
1519def canned_check_filter(method_names):
1520 filtered = {}
1521 try:
1522 for method_name in method_names:
1523 if not hasattr(presubmit_canned_checks, method_name):
1524 raise NonexistantCannedCheckFilter(method_name)
1525 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1526 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1527 yield
1528 finally:
1529 for name, method in filtered.iteritems():
1530 setattr(presubmit_canned_checks, name, method)
1531
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001532
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001533def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001534 """Runs an external program, potentially from a child process created by the
1535 multiprocessing module.
1536
1537 multiprocessing needs a top level function with a single argument.
1538 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001539 cmd_data.kwargs['stdout'] = subprocess.PIPE
1540 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1541 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001542 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001543 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001544 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001545 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001546 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001547 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001548 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1549 if code != 0:
1550 return cmd_data.message(
1551 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1552 if cmd_data.info:
1553 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001554
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001555
sbc@chromium.org013731e2015-02-26 18:28:43 +00001556def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001557 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001558 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001559 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001560 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001561 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1562 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001563 parser.add_option("-r", "--recursive", action="store_true",
1564 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001565 parser.add_option("-v", "--verbose", action="count", default=0,
1566 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001567 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001568 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001569 parser.add_option("--description", default='')
1570 parser.add_option("--issue", type='int', default=0)
1571 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001572 parser.add_option("--root", default=os.getcwd(),
1573 help="Search for PRESUBMIT.py up to this directory. "
1574 "If inherit-review-settings-ok is present in this "
1575 "directory, parent directories up to the root file "
1576 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001577 parser.add_option("--upstream",
1578 help="Git only: the base ref or upstream branch against "
1579 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001580 parser.add_option("--default_presubmit")
1581 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001582 parser.add_option("--skip_canned", action='append', default=[],
1583 help="A list of checks to skip which appear in "
1584 "presubmit_canned_checks. Can be provided multiple times "
1585 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001586 parser.add_option("--dry_run", action='store_true',
1587 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001588 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001589 parser.add_option("--gerrit_fetch", action='store_true',
1590 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001591 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1592 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001593 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1594 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001595 # These are for OAuth2 authentication for bots. See also apply_issue.py
1596 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1597 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1598
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001599 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001600 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001601 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001602
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001603 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001604 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001605 elif options.verbose:
1606 logging.basicConfig(level=logging.INFO)
1607 else:
1608 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001609
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001610 if (any((options.rietveld_url, options.rietveld_email_file,
1611 options.rietveld_fetch, options.rietveld_private_key_file))
1612 and any((options.gerrit_url, options.gerrit_fetch))):
1613 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1614 'allowed')
1615
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001616 if options.rietveld_email and options.rietveld_email_file:
1617 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1618 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001619 if options.rietveld_email_file:
1620 with open(options.rietveld_email_file, "rb") as f:
1621 options.rietveld_email = f.read().strip()
1622
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001623 change_class, files = load_files(options, args)
1624 if not change_class:
1625 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001626 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001627
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001628 rietveld_obj, gerrit_obj = None, None
1629
maruel@chromium.org239f4112011-06-03 20:08:23 +00001630 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001631 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001632 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001633 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1634 options.rietveld_url,
1635 options.rietveld_email,
1636 options.rietveld_private_key_file)
1637 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001638 rietveld_obj = rietveld.CachingRietveld(
1639 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001640 auth_config,
1641 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001642 if options.rietveld_fetch:
1643 assert options.issue
1644 props = rietveld_obj.get_issue_properties(options.issue, False)
1645 options.author = props['owner_email']
1646 options.description = props['description']
1647 logging.info('Got author: "%s"', options.author)
1648 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001649
1650 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001651 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001652 rietveld_obj = None
1653 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1654 options.author = gerrit_obj.GetChangeOwner(options.issue)
1655 options.description = gerrit_obj.GetChangeDescription(options.issue,
1656 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001657 logging.info('Got author: "%s"', options.author)
1658 logging.info('Got description: """\n%s\n"""', options.description)
1659
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001660 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001661 with canned_check_filter(options.skip_canned):
1662 results = DoPresubmitChecks(
1663 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001664 options.description,
1665 options.root,
1666 files,
1667 options.issue,
1668 options.patchset,
1669 options.author,
1670 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001671 options.commit,
1672 options.verbose,
1673 sys.stdout,
1674 sys.stdin,
1675 options.default_presubmit,
1676 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001677 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001678 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001679 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001680 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001681 except NonexistantCannedCheckFilter, e:
1682 print >> sys.stderr, (
1683 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1684 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001685 except PresubmitFailure, e:
1686 print >> sys.stderr, e
1687 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1688 print >> sys.stderr, 'If all fails, contact maruel@'
1689 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001690
1691
1692if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001693 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001694 try:
1695 sys.exit(main())
1696 except KeyboardInterrupt:
1697 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001698 sys.exit(2)