blob: 8493d273c54f760f14fae986f3ceeacaf2164922 [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 []
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000140 self._long_text = long_text.rstrip()
141
142 def handle(self, output):
143 output.write(self._message)
144 output.write('\n')
145 for index, item in enumerate(self._items):
146 output.write(' ')
147 # Write separately in case it's unicode.
148 output.write(str(item))
149 if index < len(self._items) - 1:
150 output.write(' \\')
151 output.write('\n')
152 if self._long_text:
153 output.write('\n***************\n')
154 # Write separately in case it's unicode.
155 output.write(self._long_text)
156 output.write('\n***************\n')
157 if self.fatal:
158 output.fail()
159
160
161# Top level object so multiprocessing can pickle
162# Public access through OutputApi object.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000163class _PresubmitError(_PresubmitResult):
164 """A hard presubmit error."""
165 fatal = True
166
167
168# Top level object so multiprocessing can pickle
169# Public access through OutputApi object.
170class _PresubmitPromptWarning(_PresubmitResult):
171 """An warning that prompts the user if they want to continue."""
172 should_prompt = True
173
174
175# Top level object so multiprocessing can pickle
176# Public access through OutputApi object.
177class _PresubmitNotifyResult(_PresubmitResult):
178 """Just print something to the screen -- but it's not even a warning."""
179 pass
180
181
182# Top level object so multiprocessing can pickle
183# Public access through OutputApi object.
184class _MailTextResult(_PresubmitResult):
185 """A warning that should be included in the review request email."""
186 def __init__(self, *args, **kwargs):
187 super(_MailTextResult, self).__init__()
188 raise NotImplementedError()
189
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000190class GerritAccessor(object):
191 """Limited Gerrit functionality for canned presubmit checks to work.
192
193 To avoid excessive Gerrit calls, caches the results.
194 """
195
196 def __init__(self, host):
197 self.host = host
198 self.cache = {}
199
200 def _FetchChangeDetail(self, issue):
201 # Separate function to be easily mocked in tests.
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100202 try:
203 return gerrit_util.GetChangeDetail(
204 self.host, str(issue),
Aaron Gable6f5a8d92017-04-18 14:49:05 -0700205 ['ALL_REVISIONS', 'DETAILED_LABELS', 'ALL_COMMITS'])
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +0100206 except gerrit_util.GerritError as e:
207 if e.http_status == 404:
208 raise Exception('Either Gerrit issue %s doesn\'t exist, or '
209 'no credentials to fetch issue details' % issue)
210 raise
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000211
212 def GetChangeInfo(self, issue):
213 """Returns labels and all revisions (patchsets) for this issue.
214
215 The result is a dictionary according to Gerrit REST Api.
216 https://gerrit-review.googlesource.com/Documentation/rest-api.html
217
218 However, API isn't very clear what's inside, so see tests for example.
219 """
220 assert issue
221 cache_key = int(issue)
222 if cache_key not in self.cache:
223 self.cache[cache_key] = self._FetchChangeDetail(issue)
224 return self.cache[cache_key]
225
226 def GetChangeDescription(self, issue, patchset=None):
227 """If patchset is none, fetches current patchset."""
228 info = self.GetChangeInfo(issue)
229 # info is a reference to cache. We'll modify it here adding description to
230 # it to the right patchset, if it is not yet there.
231
232 # Find revision info for the patchset we want.
233 if patchset is not None:
234 for rev, rev_info in info['revisions'].iteritems():
235 if str(rev_info['_number']) == str(patchset):
236 break
237 else:
238 raise Exception('patchset %s doesn\'t exist in issue %s' % (
239 patchset, issue))
240 else:
241 rev = info['current_revision']
242 rev_info = info['revisions'][rev]
243
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +0100244 return rev_info['commit']['message']
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000245
Mun Yong Jang603d01e2017-12-19 16:38:30 -0800246 def GetDestRef(self, issue):
247 ref = self.GetChangeInfo(issue)['branch']
248 if not ref.startswith('refs/'):
249 # NOTE: it is possible to create 'refs/x' branch,
250 # aka 'refs/heads/refs/x'. However, this is ill-advised.
251 ref = 'refs/heads/%s' % ref
252 return ref
253
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000254 def GetChangeOwner(self, issue):
255 return self.GetChangeInfo(issue)['owner']['email']
256
257 def GetChangeReviewers(self, issue, approving_only=True):
Aaron Gable8b478f02017-07-31 15:33:19 -0700258 changeinfo = self.GetChangeInfo(issue)
259 if approving_only:
260 labelinfo = changeinfo.get('labels', {}).get('Code-Review', {})
261 values = labelinfo.get('values', {}).keys()
262 try:
263 max_value = max(int(v) for v in values)
264 reviewers = [r for r in labelinfo.get('all', [])
265 if r.get('value', 0) == max_value]
266 except ValueError: # values is the empty list
267 reviewers = []
268 else:
269 reviewers = changeinfo.get('reviewers', {}).get('REVIEWER', [])
270 return [r.get('email') for r in reviewers]
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000271
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000272
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000273class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000274 """An instance of OutputApi gets passed to presubmit scripts so that they
275 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000276 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000277 PresubmitResult = _PresubmitResult
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000278 PresubmitError = _PresubmitError
279 PresubmitPromptWarning = _PresubmitPromptWarning
280 PresubmitNotifyResult = _PresubmitNotifyResult
281 MailTextResult = _MailTextResult
282
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000283 def __init__(self, is_committing):
284 self.is_committing = is_committing
Daniel Cheng7227d212017-11-17 08:12:37 -0800285 self.more_cc = []
286
287 def AppendCC(self, cc):
288 """Appends a user to cc for this change."""
289 self.more_cc.append(cc)
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000290
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000291 def PresubmitPromptOrNotify(self, *args, **kwargs):
292 """Warn the user when uploading, but only notify if committing."""
293 if self.is_committing:
294 return self.PresubmitNotifyResult(*args, **kwargs)
295 return self.PresubmitPromptWarning(*args, **kwargs)
296
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800297 def EnsureCQIncludeTrybotsAreAdded(self, cl, bots_to_include, message):
298 """Helper for any PostUploadHook wishing to add CQ_INCLUDE_TRYBOTS.
299
300 Merges the bots_to_include into the current CQ_INCLUDE_TRYBOTS list,
301 keeping it alphabetically sorted. Returns the results that should be
302 returned from the PostUploadHook.
303
304 Args:
305 cl: The git_cl.Changelist object.
306 bots_to_include: A list of strings of bots to include, in the form
307 "master:slave".
308 message: A message to be printed in the case that
309 CQ_INCLUDE_TRYBOTS was updated.
310 """
311 description = cl.GetDescription(force=True)
Aaron Gableb584c4f2017-04-26 16:28:08 -0700312 include_re = re.compile(r'^CQ_INCLUDE_TRYBOTS=(.*)$', re.M | re.I)
313
314 prior_bots = []
315 if cl.IsGerrit():
316 trybot_footers = git_footers.parse_footers(description).get(
317 git_footers.normalize_name('Cq-Include-Trybots'), [])
318 for f in trybot_footers:
319 prior_bots += [b.strip() for b in f.split(';') if b.strip()]
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800320 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700321 trybot_tags = include_re.finditer(description)
322 for t in trybot_tags:
323 prior_bots += [b.strip() for b in t.group(1).split(';') if b.strip()]
324
325 if set(prior_bots) >= set(bots_to_include):
326 return []
327 all_bots = ';'.join(sorted(set(prior_bots) | set(bots_to_include)))
328
329 if cl.IsGerrit():
330 description = git_footers.remove_footer(
331 description, 'Cq-Include-Trybots')
332 description = git_footers.add_footer(
333 description, 'Cq-Include-Trybots', all_bots,
334 before_keys=['Change-Id'])
335 else:
336 new_include_trybots = 'CQ_INCLUDE_TRYBOTS=%s' % all_bots
337 m = include_re.search(description)
338 if m:
339 description = include_re.sub(new_include_trybots, description)
Kenneth Russelldf6e7342017-04-24 17:07:41 -0700340 else:
Aaron Gableb584c4f2017-04-26 16:28:08 -0700341 description = '%s\n%s\n' % (description, new_include_trybots)
342
343 cl.UpdateDescription(description, force=True)
Kenneth Russell61e2ed42017-02-15 11:47:13 -0800344 return [self.PresubmitNotifyResult(message)]
345
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000346
347class InputApi(object):
348 """An instance of this object is passed to presubmit scripts so they can
349 know stuff about the change they're looking at.
350 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000351 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800352 # pylint: disable=no-self-use
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000353
maruel@chromium.org3410d912009-06-09 20:56:16 +0000354 # File extensions that are considered source files from a style guide
355 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000356 #
357 # Files without an extension aren't included in the list. If you want to
358 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
359 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000360 DEFAULT_WHITE_LIST = (
361 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000362 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
363 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000364 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000365 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000366 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000367 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000368 )
369
370 # Path regexp that should be excluded from being considered containing source
371 # files. Don't modify this list from a presubmit script!
372 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000373 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000374 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000375 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
376 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000377 # Output directories (just in case)
378 r".*\bDebug[\\\/].*",
379 r".*\bRelease[\\\/].*",
380 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000381 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000382 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000383 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000384 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000385 r"(|.*[\\\/])\.git[\\\/].*",
386 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000387 # There is no point in processing a patch file.
388 r".+\.diff$",
389 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000390 )
391
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000392 def __init__(self, change, presubmit_path, is_committing,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000393 rietveld_obj, verbose, gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000394 """Builds an InputApi object.
395
396 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000397 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000398 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000399 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000400 rietveld_obj: rietveld.Rietveld client object
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000401 gerrit_obj: provides basic Gerrit codereview functionality.
402 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000403 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000404 # Version number of the presubmit_support script.
405 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000406 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000407 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000408 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +0000409 self.gerrit = gerrit_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000410 self.dry_run = dry_run
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000411 # TBD
412 self.host_url = 'http://codereview.chromium.org'
413 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000414 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000415
416 # We expose various modules and functions as attributes of the input_api
417 # so that presubmit scripts don't have to import them.
Takeshi Yoshino07a6bea2017-08-02 02:44:06 +0900418 self.ast = ast
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000419 self.basename = os.path.basename
420 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000421 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000422 self.cStringIO = cStringIO
dcheng091b7db2016-06-16 01:27:51 -0700423 self.fnmatch = fnmatch
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000424 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000425 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000426 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000427 self.os_listdir = os.listdir
428 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000429 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000430 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000431 self.pickle = pickle
432 self.marshal = marshal
433 self.re = re
434 self.subprocess = subprocess
435 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000436 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000437 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000438 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000439 self.urllib2 = urllib2
440
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000441 # To easily fork python.
442 self.python_executable = sys.executable
443 self.environ = os.environ
444
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000445 # InputApi.platform is the platform you're currently running on.
446 self.platform = sys.platform
447
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000448 self.cpu_count = multiprocessing.cpu_count()
449
Aaron Gable5254da12017-09-06 21:05:39 +0000450 # this is done here because in RunTests, the current working directory has
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000451 # changed, which causes Pool() to explode fantastically when run on windows
452 # (because it tries to load the __main__ module, which imports lots of
453 # things relative to the current working directory).
Aaron Gable5254da12017-09-06 21:05:39 +0000454 self._run_tests_pool = multiprocessing.Pool(self.cpu_count)
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000455
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000456 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000457 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000458
459 # We carry the canned checks so presubmit scripts can easily use them.
460 self.canned_checks = presubmit_canned_checks
461
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100462 # Temporary files we must manually remove at the end of a run.
463 self._named_temporary_files = []
Jochen Eisinger72606f82017-04-04 10:44:18 +0200464
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000465 # TODO(dpranke): figure out a list of all approved owners for a repo
466 # in order to be able to handle wildcard OWNERS files?
467 self.owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +0200468 fopen=file, os_path=self.os_path)
Jochen Eisinger76f5fc62017-04-07 16:27:46 +0200469 self.owners_finder = owners_finder.OwnersFinder
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000470 self.verbose = verbose
Dan Jacques94652a32017-10-09 23:18:46 -0400471 self.is_windows = sys.platform == 'win32'
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000472 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000473
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000474 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000475 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000476 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800477 # pylint: disable=protected-access
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000478 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000479 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000480 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
481 for (a, b, header) in cpplint._re_pattern_templates
482 ]
483
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000484 def PresubmitLocalPath(self):
485 """Returns the local path of the presubmit script currently being run.
486
487 This is useful if you don't want to hard-code absolute paths in the
488 presubmit script. For example, It can be used to find another file
489 relative to the PRESUBMIT.py script, so the whole tree can be branched and
490 the presubmit script still works, without editing its content.
491 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000492 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000493
agable0b65e732016-11-22 09:25:46 -0800494 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000495 """Same as input_api.change.AffectedFiles() except only lists files
496 (and optionally directories) in the same directory as the current presubmit
497 script, or subdirectories thereof.
498 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000499 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000500 if len(dir_with_slash) == 1:
501 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000502
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000503 return filter(
504 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
agable0b65e732016-11-22 09:25:46 -0800505 self.change.AffectedFiles(include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000506
agable0b65e732016-11-22 09:25:46 -0800507 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000508 """Returns local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800509 paths = [af.LocalPath() for af in self.AffectedFiles()]
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000510 logging.debug("LocalPaths: %s", paths)
511 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000512
agable0b65e732016-11-22 09:25:46 -0800513 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000514 """Returns absolute local paths of input_api.AffectedFiles()."""
agable0b65e732016-11-22 09:25:46 -0800515 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000516
agable0b65e732016-11-22 09:25:46 -0800517 def AffectedTestableFiles(self, include_deletes=None):
518 """Same as input_api.change.AffectedTestableFiles() except only lists files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000519 in the same directory as the current presubmit script, or subdirectories
520 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000521 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000522 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800523 warn("AffectedTestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000524 " is deprecated and ignored" % str(include_deletes),
525 category=DeprecationWarning,
526 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800527 return filter(lambda x: x.IsTestableFile(),
528 self.AffectedFiles(include_deletes=False))
529
530 def AffectedTextFiles(self, include_deletes=None):
531 """An alias to AffectedTestableFiles for backwards compatibility."""
532 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000533
maruel@chromium.org3410d912009-06-09 20:56:16 +0000534 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
535 """Filters out files that aren't considered "source file".
536
537 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
538 and InputApi.DEFAULT_BLACK_LIST is used respectively.
539
540 The lists will be compiled as regular expression and
541 AffectedFile.LocalPath() needs to pass both list.
542
543 Note: Copy-paste this function to suit your needs or use a lambda function.
544 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000545 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000546 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000547 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000548 if self.re.match(item, local_path):
maruel@chromium.org3410d912009-06-09 20:56:16 +0000549 return True
550 return False
551 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
552 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
553
554 def AffectedSourceFiles(self, source_file):
agable0b65e732016-11-22 09:25:46 -0800555 """Filter the list of AffectedTestableFiles by the function source_file.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000556
557 If source_file is None, InputApi.FilterSourceFile() is used.
558 """
559 if not source_file:
560 source_file = self.FilterSourceFile
agable0b65e732016-11-22 09:25:46 -0800561 return filter(source_file, self.AffectedTestableFiles())
maruel@chromium.org3410d912009-06-09 20:56:16 +0000562
563 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000564 """An iterator over all text lines in "new" version of changed files.
565
566 Only lists lines from new or modified text files in the change that are
567 contained by the directory of the currently executing presubmit script.
568
569 This is useful for doing line-by-line regex checks, like checking for
570 trailing whitespace.
571
572 Yields:
573 a 3 tuple:
574 the AffectedFile instance of the current file;
575 integer line number (1-based); and
576 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000577
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000578 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000579 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000580 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000581 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000582
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000583 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000584 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000585
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000586 Deny reading anything outside the repository.
587 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000588 if isinstance(file_item, AffectedFile):
589 file_item = file_item.AbsoluteLocalPath()
590 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000591 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000592 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000593
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +0100594 def CreateTemporaryFile(self, **kwargs):
595 """Returns a named temporary file that must be removed with a call to
596 RemoveTemporaryFiles().
597
598 All keyword arguments are forwarded to tempfile.NamedTemporaryFile(),
599 except for |delete|, which is always set to False.
600
601 Presubmit checks that need to create a temporary file and pass it for
602 reading should use this function instead of NamedTemporaryFile(), as
603 Windows fails to open a file that is already open for writing.
604
605 with input_api.CreateTemporaryFile() as f:
606 f.write('xyz')
607 f.close()
608 input_api.subprocess.check_output(['script-that', '--reads-from',
609 f.name])
610
611
612 Note that callers of CreateTemporaryFile() should not worry about removing
613 any temporary file; this is done transparently by the presubmit handling
614 code.
615 """
616 if 'delete' in kwargs:
617 # Prevent users from passing |delete|; we take care of file deletion
618 # ourselves and this prevents unintuitive error messages when we pass
619 # delete=False and 'delete' is also in kwargs.
620 raise TypeError('CreateTemporaryFile() does not take a "delete" '
621 'argument, file deletion is handled automatically by '
622 'the same presubmit_support code that creates InputApi '
623 'objects.')
624 temp_file = self.tempfile.NamedTemporaryFile(delete=False, **kwargs)
625 self._named_temporary_files.append(temp_file.name)
626 return temp_file
627
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000628 @property
629 def tbr(self):
630 """Returns if a change is TBR'ed."""
Jeremy Romandce22502017-06-20 15:37:29 -0400631 return 'TBR' in self.change.tags or self.change.TBRsFromDescription()
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000632
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000633 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000634 tests = []
635 msgs = []
636 for t in tests_mix:
637 if isinstance(t, OutputApi.PresubmitResult):
638 msgs.append(t)
639 else:
640 assert issubclass(t.message, _PresubmitResult)
641 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000642 if self.verbose:
643 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000644 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000645 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000646 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000647 else:
648 msgs.extend(map(CallCommand, tests))
649 return [m for m in msgs if m]
650
scottmg86099d72016-09-01 09:16:51 -0700651 def ShutdownPool(self):
652 self._run_tests_pool.close()
653 self._run_tests_pool.join()
654 self._run_tests_pool = None
655
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000656
nick@chromium.orgff526192013-06-10 19:30:26 +0000657class _DiffCache(object):
658 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000659 def __init__(self, upstream=None):
660 """Stores the upstream revision against which all diffs will be computed."""
661 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000662
663 def GetDiff(self, path, local_root):
664 """Get the diff for a particular path."""
665 raise NotImplementedError()
666
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700667 def GetOldContents(self, path, local_root):
668 """Get the old version for a particular path."""
669 raise NotImplementedError()
670
nick@chromium.orgff526192013-06-10 19:30:26 +0000671
nick@chromium.orgff526192013-06-10 19:30:26 +0000672class _GitDiffCache(_DiffCache):
673 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000674 def __init__(self, upstream):
675 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000676 self._diffs_by_file = None
677
678 def GetDiff(self, path, local_root):
679 if not self._diffs_by_file:
680 # Compute a single diff for all files and parse the output; should
681 # with git this is much faster than computing one diff for each file.
682 diffs = {}
683
684 # Don't specify any filenames below, because there are command line length
685 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000686 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
687 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000688
689 # This regex matches the path twice, separated by a space. Note that
690 # filename itself may contain spaces.
691 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
692 current_diff = []
693 keep_line_endings = True
694 for x in unified_diff.splitlines(keep_line_endings):
695 match = file_marker.match(x)
696 if match:
697 # Marks the start of a new per-file section.
698 diffs[match.group('filename')] = current_diff = [x]
699 elif x.startswith('diff --git'):
700 raise PresubmitFailure('Unexpected diff line: %s' % x)
701 else:
702 current_diff.append(x)
703
704 self._diffs_by_file = dict(
705 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
706
707 if path not in self._diffs_by_file:
708 raise PresubmitFailure(
709 'Unified diff did not contain entry for file %s' % path)
710
711 return self._diffs_by_file[path]
712
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700713 def GetOldContents(self, path, local_root):
714 return scm.GIT.GetOldContents(local_root, path, branch=self._upstream)
715
nick@chromium.orgff526192013-06-10 19:30:26 +0000716
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717class AffectedFile(object):
718 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000719
720 DIFF_CACHE = _DiffCache
721
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000722 # Method could be a function
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800723 # pylint: disable=no-self-use
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000724 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000725 self._path = path
726 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000727 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000728 self._is_directory = None
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000729 self._cached_changed_contents = None
730 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000731 self._diff_cache = diff_cache
tobiasjs2836bcf2016-08-16 04:08:16 -0700732 logging.debug('%s(%s)', self.__class__.__name__, self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000733
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000734 def LocalPath(self):
735 """Returns the path of this file on the local disk relative to client root.
Andrew Grieve92b8b992017-11-02 09:42:24 -0400736
737 This should be used for error messages but not for accessing files,
738 because presubmit checks are run with CWD=PresubmitLocalPath() (which is
739 often != client root).
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000740 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000741 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742
743 def AbsoluteLocalPath(self):
744 """Returns the absolute path of this file on the local disk.
745 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000746 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000747
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000748 def Action(self):
749 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000750 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000751
agable0b65e732016-11-22 09:25:46 -0800752 def IsTestableFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000753 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000754
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000755 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000756 raise NotImplementedError() # Implement when needed
757
agable0b65e732016-11-22 09:25:46 -0800758 def IsTextFile(self):
759 """An alias to IsTestableFile for backwards compatibility."""
760 return self.IsTestableFile()
761
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700762 def OldContents(self):
763 """Returns an iterator over the lines in the old version of file.
764
Daniel Cheng2da34fe2017-03-21 20:42:12 -0700765 The old version is the file before any modifications in the user's
766 workspace, i.e. the "left hand side".
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700767
768 Contents will be empty if the file is a directory or does not exist.
769 Note: The carriage returns (LF or CR) are stripped off.
770 """
771 return self._diff_cache.GetOldContents(self.LocalPath(),
772 self._local_root).splitlines()
773
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000774 def NewContents(self):
775 """Returns an iterator over the lines in the new version of file.
776
777 The new version is the file in the user's workspace, i.e. the "right hand
778 side".
779
780 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000781 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000782 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000783 if self._cached_new_contents is None:
784 self._cached_new_contents = []
agable0b65e732016-11-22 09:25:46 -0800785 try:
786 self._cached_new_contents = gclient_utils.FileRead(
787 self.AbsoluteLocalPath(), 'rU').splitlines()
788 except IOError:
789 pass # File not found? That's fine; maybe it was deleted.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000790 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000791
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000792 def ChangedContents(self):
793 """Returns a list of tuples (line number, line text) of all new lines.
794
795 This relies on the scm diff output describing each changed code section
796 with a line of the form
797
798 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
799 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000800 if self._cached_changed_contents is not None:
801 return self._cached_changed_contents[:]
802 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000803 line_num = 0
804
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000805 for line in self.GenerateScmDiff().splitlines():
806 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
807 if m:
808 line_num = int(m.groups(1)[0])
809 continue
810 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000811 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000812 if not line.startswith('-'):
813 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000814 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000815
maruel@chromium.org5de13972009-06-10 18:16:06 +0000816 def __str__(self):
817 return self.LocalPath()
818
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000819 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000820 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000821
maruel@chromium.org58407af2011-04-12 23:15:57 +0000822
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000823class GitAffectedFile(AffectedFile):
824 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000825 # Method 'NNN' is abstract in class 'NNN' but is not overridden
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -0800826 # pylint: disable=abstract-method
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000827
nick@chromium.orgff526192013-06-10 19:30:26 +0000828 DIFF_CACHE = _GitDiffCache
829
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000830 def __init__(self, *args, **kwargs):
831 AffectedFile.__init__(self, *args, **kwargs)
832 self._server_path = None
agable0b65e732016-11-22 09:25:46 -0800833 self._is_testable_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000834
agable0b65e732016-11-22 09:25:46 -0800835 def IsTestableFile(self):
836 if self._is_testable_file is None:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000837 if self.Action() == 'D':
agable0b65e732016-11-22 09:25:46 -0800838 # A deleted file is not testable.
839 self._is_testable_file = False
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000840 else:
agable0b65e732016-11-22 09:25:46 -0800841 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath())
842 return self._is_testable_file
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000843
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000844
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000845class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000846 """Describe a change.
847
848 Used directly by the presubmit scripts to query the current change being
849 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000850
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000851 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000852 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000853 self.KEY: equivalent to tags['KEY']
854 """
855
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000856 _AFFECTED_FILES = AffectedFile
857
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000858 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000859 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000860 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000861 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000862
maruel@chromium.org58407af2011-04-12 23:15:57 +0000863 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000864 self, name, description, local_root, files, issue, patchset, author,
865 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000866 if files is None:
867 files = []
868 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000869 # Convert root into an absolute path.
870 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000871 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000872 self.issue = issue
873 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000874 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000875
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000876 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000877 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000878 self._description_without_tags = ''
879 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000880
maruel@chromium.orge085d812011-10-10 19:49:15 +0000881 assert all(
882 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
883
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000884 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000885 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000886 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
887 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000888 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000889
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000890 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000891 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000892 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000893
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000894 def DescriptionText(self):
895 """Returns the user-entered changelist description, minus tags.
896
897 Any line in the user-provided description starting with e.g. "FOO="
898 (whitespace permitted before and around) is considered a tag line. Such
899 lines are stripped out of the description this function returns.
900 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000901 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000902
903 def FullDescriptionText(self):
904 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000905 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000906
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000907 def SetDescriptionText(self, description):
908 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000909
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000910 Also updates the list of tags."""
911 self._full_description = description
912
913 # From the description text, build up a dictionary of key/value pairs
914 # plus the description minus all key/value or "tag" lines.
915 description_without_tags = []
916 self.tags = {}
917 for line in self._full_description.splitlines():
918 m = self.TAG_LINE_RE.match(line)
919 if m:
920 self.tags[m.group('key')] = m.group('value')
921 else:
922 description_without_tags.append(line)
923
924 # Change back to text and remove whitespace at end.
925 self._description_without_tags = (
926 '\n'.join(description_without_tags).rstrip())
927
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000928 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000929 """Returns the repository (checkout) root directory for this change,
930 as an absolute path.
931 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000932 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000933
934 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000935 """Return tags directly as attributes on the object."""
936 if not re.match(r"^[A-Z_]*$", attr):
937 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000938 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000939
Aaron Gablefc03e672017-05-15 14:09:42 -0700940 def BugsFromDescription(self):
941 """Returns all bugs referenced in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700942 tags = [b.strip() for b in self.tags.get('BUG', '').split(',') if b.strip()]
943 footers = git_footers.parse_footers(self._full_description).get('Bug', [])
944 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -0700945
946 def ReviewersFromDescription(self):
947 """Returns all reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700948 # We don't support a "R:" git-footer for reviewers; that is in metadata.
949 tags = [r.strip() for r in self.tags.get('R', '').split(',') if r.strip()]
950 return sorted(set(tags))
Aaron Gablefc03e672017-05-15 14:09:42 -0700951
952 def TBRsFromDescription(self):
953 """Returns all TBR reviewers listed in the commit description."""
Aaron Gable12ef5012017-05-15 14:29:00 -0700954 tags = [r.strip() for r in self.tags.get('TBR', '').split(',') if r.strip()]
955 # TODO(agable): Remove support for 'Tbr:' when TBRs are programmatically
956 # determined by self-CR+1s.
957 footers = git_footers.parse_footers(self._full_description).get('Tbr', [])
958 return sorted(set(tags + footers))
Aaron Gablefc03e672017-05-15 14:09:42 -0700959
960 # TODO(agable): Delete these once we're sure they're unused.
961 @property
962 def BUG(self):
963 return ','.join(self.BugsFromDescription())
964 @property
965 def R(self):
966 return ','.join(self.ReviewersFromDescription())
967 @property
968 def TBR(self):
969 return ','.join(self.TBRsFromDescription())
970
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000971 def AllFiles(self, root=None):
972 """List all files under source control in the repo."""
973 raise NotImplementedError()
974
agable0b65e732016-11-22 09:25:46 -0800975 def AffectedFiles(self, include_deletes=True, file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000976 """Returns a list of AffectedFile instances for all files in the change.
977
978 Args:
979 include_deletes: If false, deleted files will be filtered out.
sail@chromium.org5538e022011-05-12 17:53:16 +0000980 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000981
982 Returns:
983 [AffectedFile(path, action), AffectedFile(path, action)]
984 """
agable0b65e732016-11-22 09:25:46 -0800985 affected = filter(file_filter, self._affected_files)
sail@chromium.org5538e022011-05-12 17:53:16 +0000986
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000987 if include_deletes:
988 return affected
Lei Zhang9611c4c2017-04-04 01:41:56 -0700989 return filter(lambda x: x.Action() != 'D', affected)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000990
agable0b65e732016-11-22 09:25:46 -0800991 def AffectedTestableFiles(self, include_deletes=None):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000992 """Return a list of the existing text files in a change."""
993 if include_deletes is not None:
agable0b65e732016-11-22 09:25:46 -0800994 warn("AffectedTeestableFiles(include_deletes=%s)"
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000995 " is deprecated and ignored" % str(include_deletes),
996 category=DeprecationWarning,
997 stacklevel=2)
agable0b65e732016-11-22 09:25:46 -0800998 return filter(lambda x: x.IsTestableFile(),
999 self.AffectedFiles(include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001000
agable0b65e732016-11-22 09:25:46 -08001001 def AffectedTextFiles(self, include_deletes=None):
1002 """An alias to AffectedTestableFiles for backwards compatibility."""
1003 return self.AffectedTestableFiles(include_deletes=include_deletes)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001004
agable0b65e732016-11-22 09:25:46 -08001005 def LocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001007 return [af.LocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008
agable0b65e732016-11-22 09:25:46 -08001009 def AbsoluteLocalPaths(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010 """Convenience function."""
agable0b65e732016-11-22 09:25:46 -08001011 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001012
1013 def RightHandSideLines(self):
1014 """An iterator over all text lines in "new" version of changed files.
1015
1016 Lists lines from new or modified text files in the change.
1017
1018 This is useful for doing line-by-line regex checks, like checking for
1019 trailing whitespace.
1020
1021 Yields:
1022 a 3 tuple:
1023 the AffectedFile instance of the current file;
1024 integer line number (1-based); and
1025 the contents of the line as a string.
1026 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001027 return _RightHandSideLinesImpl(
1028 x for x in self.AffectedFiles(include_deletes=False)
agable0b65e732016-11-22 09:25:46 -08001029 if x.IsTestableFile())
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001030
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02001031 def OriginalOwnersFiles(self):
1032 """A map from path names of affected OWNERS files to their old content."""
1033 def owners_file_filter(f):
1034 return 'OWNERS' in os.path.split(f.LocalPath())[1]
1035 files = self.AffectedFiles(file_filter=owners_file_filter)
1036 return dict([(f.LocalPath(), f.OldContents()) for f in files])
1037
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001038
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001039class GitChange(Change):
1040 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +00001041 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +00001042
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001043 def AllFiles(self, root=None):
1044 """List all files under source control in the repo."""
1045 root = root or self.RepositoryRoot()
1046 return subprocess.check_output(
Aaron Gable7817f022017-12-12 09:43:17 -08001047 ['git', '-c', 'core.quotePath=false', 'ls-files', '--', '.'],
1048 cwd=root).splitlines()
agable@chromium.org40a3d0b2014-05-15 01:59:16 +00001049
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001050
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001051def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001052 """Finds all presubmit files that apply to a given set of source files.
1053
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001054 If inherit-review-settings-ok is present right under root, looks for
1055 PRESUBMIT.py in directories enclosing root.
1056
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001057 Args:
1058 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001059 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001060
1061 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001062 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001063 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001064 files = [normpath(os.path.join(root, f)) for f in files]
1065
1066 # List all the individual directories containing files.
1067 directories = set([os.path.dirname(f) for f in files])
1068
1069 # Ignore root if inherit-review-settings-ok is present.
1070 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1071 root = None
1072
1073 # Collect all unique directories that may contain PRESUBMIT.py.
1074 candidates = set()
1075 for directory in directories:
1076 while True:
1077 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001078 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001079 candidates.add(directory)
1080 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001081 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001082 parent_dir = os.path.dirname(directory)
1083 if parent_dir == directory:
1084 # We hit the system root directory.
1085 break
1086 directory = parent_dir
1087
1088 # Look for PRESUBMIT.py in all candidate directories.
1089 results = []
1090 for directory in sorted(list(candidates)):
tobiasjsff061c02016-08-17 03:23:57 -07001091 try:
1092 for f in os.listdir(directory):
1093 p = os.path.join(directory, f)
1094 if os.path.isfile(p) and re.match(
1095 r'PRESUBMIT.*\.py$', f) and not f.startswith('PRESUBMIT_test'):
1096 results.append(p)
1097 except OSError:
1098 pass
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001099
tobiasjs2836bcf2016-08-16 04:08:16 -07001100 logging.debug('Presubmit files: %s', ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001101 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001102
1103
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001104class GetTryMastersExecuter(object):
1105 @staticmethod
1106 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1107 """Executes GetPreferredTryMasters() from a single presubmit script.
1108
1109 Args:
1110 script_text: The text of the presubmit script.
1111 presubmit_path: Project script to run.
1112 project: Project name to pass to presubmit script for bot selection.
1113
1114 Return:
1115 A map of try masters to map of builders to set of tests.
1116 """
1117 context = {}
1118 try:
1119 exec script_text in context
1120 except Exception, e:
1121 raise PresubmitFailure('"%s" had an exception.\n%s'
1122 % (presubmit_path, e))
1123
1124 function_name = 'GetPreferredTryMasters'
1125 if function_name not in context:
1126 return {}
1127 get_preferred_try_masters = context[function_name]
1128 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1129 raise PresubmitFailure(
1130 'Expected function "GetPreferredTryMasters" to take two arguments.')
1131 return get_preferred_try_masters(project, change)
1132
1133
rmistry@google.com5626a922015-02-26 14:03:30 +00001134class GetPostUploadExecuter(object):
1135 @staticmethod
1136 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1137 """Executes PostUploadHook() from a single presubmit script.
1138
1139 Args:
1140 script_text: The text of the presubmit script.
1141 presubmit_path: Project script to run.
1142 cl: The Changelist object.
1143 change: The Change object.
1144
1145 Return:
1146 A list of results objects.
1147 """
1148 context = {}
1149 try:
1150 exec script_text in context
1151 except Exception, e:
1152 raise PresubmitFailure('"%s" had an exception.\n%s'
1153 % (presubmit_path, e))
1154
1155 function_name = 'PostUploadHook'
1156 if function_name not in context:
1157 return {}
1158 post_upload_hook = context[function_name]
1159 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1160 raise PresubmitFailure(
1161 'Expected function "PostUploadHook" to take three arguments.')
1162 return post_upload_hook(cl, change, OutputApi(False))
1163
1164
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001165def _MergeMasters(masters1, masters2):
1166 """Merges two master maps. Merges also the tests of each builder."""
1167 result = {}
1168 for (master, builders) in itertools.chain(masters1.iteritems(),
1169 masters2.iteritems()):
1170 new_builders = result.setdefault(master, {})
1171 for (builder, tests) in builders.iteritems():
1172 new_builders.setdefault(builder, set([])).update(tests)
1173 return result
1174
1175
1176def DoGetTryMasters(change,
1177 changed_files,
1178 repository_root,
1179 default_presubmit,
1180 project,
1181 verbose,
1182 output_stream):
1183 """Get the list of try masters from the presubmit scripts.
1184
1185 Args:
1186 changed_files: List of modified files.
1187 repository_root: The repository root.
1188 default_presubmit: A default presubmit script to execute in any case.
1189 project: Optional name of a project used in selecting trybots.
1190 verbose: Prints debug info.
1191 output_stream: A stream to write debug output to.
1192
1193 Return:
1194 Map of try masters to map of builders to set of tests.
1195 """
1196 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1197 if not presubmit_files and verbose:
1198 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1199 results = {}
1200 executer = GetTryMastersExecuter()
1201
1202 if default_presubmit:
1203 if verbose:
1204 output_stream.write("Running default presubmit script.\n")
1205 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1206 results = _MergeMasters(results, executer.ExecPresubmitScript(
1207 default_presubmit, fake_path, project, change))
1208 for filename in presubmit_files:
1209 filename = os.path.abspath(filename)
1210 if verbose:
1211 output_stream.write("Running %s\n" % filename)
1212 # Accept CRLF presubmit script.
1213 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1214 results = _MergeMasters(results, executer.ExecPresubmitScript(
1215 presubmit_script, filename, project, change))
1216
1217 # Make sets to lists again for later JSON serialization.
1218 for builders in results.itervalues():
1219 for builder in builders:
1220 builders[builder] = list(builders[builder])
1221
1222 if results and verbose:
1223 output_stream.write('%s\n' % str(results))
1224 return results
1225
1226
rmistry@google.com5626a922015-02-26 14:03:30 +00001227def DoPostUploadExecuter(change,
1228 cl,
1229 repository_root,
1230 verbose,
1231 output_stream):
1232 """Execute the post upload hook.
1233
1234 Args:
1235 change: The Change object.
1236 cl: The Changelist object.
1237 repository_root: The repository root.
1238 verbose: Prints debug info.
1239 output_stream: A stream to write debug output to.
1240 """
1241 presubmit_files = ListRelevantPresubmitFiles(
1242 change.LocalPaths(), repository_root)
1243 if not presubmit_files and verbose:
1244 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1245 results = []
1246 executer = GetPostUploadExecuter()
1247 # The root presubmit file should be executed after the ones in subdirectories.
1248 # i.e. the specific post upload hooks should run before the general ones.
1249 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1250 presubmit_files.reverse()
1251
1252 for filename in presubmit_files:
1253 filename = os.path.abspath(filename)
1254 if verbose:
1255 output_stream.write("Running %s\n" % filename)
1256 # Accept CRLF presubmit script.
1257 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1258 results.extend(executer.ExecPresubmitScript(
1259 presubmit_script, filename, cl, change))
1260 output_stream.write('\n')
1261 if results:
1262 output_stream.write('** Post Upload Hook Messages **\n')
1263 for result in results:
1264 result.handle(output_stream)
1265 output_stream.write('\n')
1266
1267 return results
1268
1269
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001270class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001271 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001272 gerrit_obj=None, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001273 """
1274 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001275 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001276 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001277 rietveld_obj: rietveld.Rietveld client object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001278 gerrit_obj: provides basic Gerrit codereview functionality.
1279 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001280 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001281 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001282 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001283 self.rietveld = rietveld_obj
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001284 self.gerrit = gerrit_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001285 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001286 self.dry_run = dry_run
Daniel Cheng7227d212017-11-17 08:12:37 -08001287 self.more_cc = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001288
1289 def ExecPresubmitScript(self, script_text, presubmit_path):
1290 """Executes a single presubmit script.
1291
1292 Args:
1293 script_text: The text of the presubmit script.
1294 presubmit_path: The path to the presubmit file (this will be reported via
1295 input_api.PresubmitLocalPath()).
1296
1297 Return:
1298 A list of result objects, empty if no problems.
1299 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001300
chase@chromium.org8e416c82009-10-06 04:30:44 +00001301 # Change to the presubmit file's directory to support local imports.
1302 main_path = os.getcwd()
1303 os.chdir(os.path.dirname(presubmit_path))
1304
1305 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001306 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001307 self.rietveld, self.verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001308 gerrit_obj=self.gerrit, dry_run=self.dry_run)
Daniel Cheng7227d212017-11-17 08:12:37 -08001309 output_api = OutputApi(self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001310 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001311 try:
1312 exec script_text in context
1313 except Exception, e:
1314 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001315
1316 # These function names must change if we make substantial changes to
1317 # the presubmit API that are not backwards compatible.
1318 if self.committing:
1319 function_name = 'CheckChangeOnCommit'
1320 else:
1321 function_name = 'CheckChangeOnUpload'
1322 if function_name in context:
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +01001323 try:
Daniel Cheng7227d212017-11-17 08:12:37 -08001324 context['__args'] = (input_api, output_api)
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +01001325 logging.debug('Running %s in %s', function_name, presubmit_path)
1326 result = eval(function_name + '(*__args)', context)
1327 logging.debug('Running %s done.', function_name)
Daniel Chengd36fce42017-11-21 21:52:52 -08001328 self.more_cc.extend(output_api.more_cc)
Raphael Kubo da Costaf2d16152017-11-10 18:07:58 +01001329 finally:
1330 map(os.remove, input_api._named_temporary_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001331 if not (isinstance(result, types.TupleType) or
1332 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001333 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001334 'Presubmit functions must return a tuple or list')
1335 for item in result:
1336 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001337 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001338 'All presubmit results must be of types derived from '
1339 'output_api.PresubmitResult')
1340 else:
1341 result = () # no error since the script doesn't care about current event.
1342
scottmg86099d72016-09-01 09:16:51 -07001343 input_api.ShutdownPool()
1344
chase@chromium.org8e416c82009-10-06 04:30:44 +00001345 # Return the process to the original working directory.
1346 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001347 return result
1348
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001349def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001350 committing,
1351 verbose,
1352 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001353 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001354 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001355 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001356 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001357 gerrit_obj=None,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001358 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001359 """Runs all presubmit checks that apply to the files in the change.
1360
1361 This finds all PRESUBMIT.py files in directories enclosing the files in the
1362 change (up to the repository root) and calls the relevant entrypoint function
1363 depending on whether the change is being committed or uploaded.
1364
1365 Prints errors, warnings and notifications. Prompts the user for warnings
1366 when needed.
1367
1368 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001369 change: The Change object.
agable92bec4f2016-08-24 09:27:27 -07001370 committing: True if 'git cl land' is running, False if 'git cl upload' is.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001371 verbose: Prints debug info.
1372 output_stream: A stream to write output from presubmit tests to.
1373 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001374 default_presubmit: A default presubmit script to execute in any case.
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001375 may_prompt: Enable (y/n) questions on warning or error. If False,
1376 any questions are answered with yes by default.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001377 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001378 gerrit_obj: provides basic Gerrit codereview functionality.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001379 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001380
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001381 Warning:
1382 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1383 SHOULD be sys.stdin.
1384
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001385 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001386 A PresubmitOutput object. Use output.should_continue() to figure out
1387 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001388 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001389 old_environ = os.environ
1390 try:
1391 # Make sure python subprocesses won't generate .pyc files.
1392 os.environ = os.environ.copy()
1393 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001394
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001395 output = PresubmitOutput(input_stream, output_stream)
1396 if committing:
1397 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001398 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001399 output.write("Running presubmit upload checks ...\n")
1400 start_time = time.time()
1401 presubmit_files = ListRelevantPresubmitFiles(
agable0b65e732016-11-22 09:25:46 -08001402 change.AbsoluteLocalPaths(), change.RepositoryRoot())
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001403 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001404 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001405 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001406 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001407 gerrit_obj, dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001408 if default_presubmit:
1409 if verbose:
1410 output.write("Running default presubmit script.\n")
1411 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1412 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1413 for filename in presubmit_files:
1414 filename = os.path.abspath(filename)
1415 if verbose:
1416 output.write("Running %s\n" % filename)
1417 # Accept CRLF presubmit script.
1418 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1419 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001420
Daniel Cheng7227d212017-11-17 08:12:37 -08001421 output.more_cc.extend(executer.more_cc)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001422 errors = []
1423 notifications = []
1424 warnings = []
1425 for result in results:
1426 if result.fatal:
1427 errors.append(result)
1428 elif result.should_prompt:
1429 warnings.append(result)
1430 else:
1431 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001432
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001433 output.write('\n')
1434 for name, items in (('Messages', notifications),
1435 ('Warnings', warnings),
1436 ('ERRORS', errors)):
1437 if items:
1438 output.write('** Presubmit %s **\n' % name)
1439 for item in items:
1440 item.handle(output)
1441 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001442
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001443 total_time = time.time() - start_time
1444 if total_time > 1.0:
1445 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001446
Quinten Yearsley516fe7f2016-12-14 11:50:18 -08001447 if errors:
1448 output.fail()
1449 elif warnings:
1450 output.write('There were presubmit warnings. ')
1451 if may_prompt:
1452 output.prompt_yes_no('Are you sure you wish to continue? (y/N): ')
1453 else:
1454 output.write('Presubmit checks passed.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001455
1456 global _ASKED_FOR_FEEDBACK
1457 # Ask for feedback one time out of 5.
1458 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001459 output.write(
1460 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1461 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1462 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001463 _ASKED_FOR_FEEDBACK = True
1464 return output
1465 finally:
1466 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001467
1468
1469def ScanSubDirs(mask, recursive):
1470 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001471 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
Lei Zhang9611c4c2017-04-04 01:41:56 -07001472
1473 results = []
1474 for root, dirs, files in os.walk('.'):
1475 if '.svn' in dirs:
1476 dirs.remove('.svn')
1477 if '.git' in dirs:
1478 dirs.remove('.git')
1479 for name in files:
1480 if fnmatch.fnmatch(name, mask):
1481 results.append(os.path.join(root, name))
1482 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001483
1484
1485def ParseFiles(args, recursive):
tobiasjs2836bcf2016-08-16 04:08:16 -07001486 logging.debug('Searching for %s', args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001487 files = []
1488 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001489 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001490 return files
1491
1492
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001493def load_files(options, args):
1494 """Tries to determine the SCM."""
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001495 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001496 if args:
1497 files = ParseFiles(args, options.recursive)
agable0b65e732016-11-22 09:25:46 -08001498 change_scm = scm.determine_scm(options.root)
1499 if change_scm == 'git':
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001500 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001501 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001502 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001503 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001504 else:
tobiasjs2836bcf2016-08-16 04:08:16 -07001505 logging.info('Doesn\'t seem under source control. Got %d files', len(args))
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001506 if not files:
1507 return None, None
1508 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001509 return change_class, files
1510
1511
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001512class NonexistantCannedCheckFilter(Exception):
1513 pass
1514
1515
1516@contextlib.contextmanager
1517def canned_check_filter(method_names):
1518 filtered = {}
1519 try:
1520 for method_name in method_names:
1521 if not hasattr(presubmit_canned_checks, method_name):
1522 raise NonexistantCannedCheckFilter(method_name)
1523 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1524 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1525 yield
1526 finally:
1527 for name, method in filtered.iteritems():
1528 setattr(presubmit_canned_checks, name, method)
1529
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001530
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001531def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001532 """Runs an external program, potentially from a child process created by the
1533 multiprocessing module.
1534
1535 multiprocessing needs a top level function with a single argument.
1536 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001537 cmd_data.kwargs['stdout'] = subprocess.PIPE
1538 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1539 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001540 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001541 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001542 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001543 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001544 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001545 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001546 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1547 if code != 0:
1548 return cmd_data.message(
1549 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1550 if cmd_data.info:
1551 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001552
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001553
sbc@chromium.org013731e2015-02-26 18:28:43 +00001554def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001555 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001556 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001557 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001558 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001559 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1560 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001561 parser.add_option("-r", "--recursive", action="store_true",
1562 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001563 parser.add_option("-v", "--verbose", action="count", default=0,
1564 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001565 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001566 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001567 parser.add_option("--description", default='')
1568 parser.add_option("--issue", type='int', default=0)
1569 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001570 parser.add_option("--root", default=os.getcwd(),
1571 help="Search for PRESUBMIT.py up to this directory. "
1572 "If inherit-review-settings-ok is present in this "
1573 "directory, parent directories up to the root file "
1574 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001575 parser.add_option("--upstream",
1576 help="Git only: the base ref or upstream branch against "
1577 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001578 parser.add_option("--default_presubmit")
1579 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001580 parser.add_option("--skip_canned", action='append', default=[],
1581 help="A list of checks to skip which appear in "
1582 "presubmit_canned_checks. Can be provided multiple times "
1583 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001584 parser.add_option("--dry_run", action='store_true',
1585 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001586 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001587 parser.add_option("--gerrit_fetch", action='store_true',
1588 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001589 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1590 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001591 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1592 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001593 # These are for OAuth2 authentication for bots. See also apply_issue.py
1594 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1595 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1596
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001597 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001598 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001599 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001600
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001601 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001602 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001603 elif options.verbose:
1604 logging.basicConfig(level=logging.INFO)
1605 else:
1606 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001607
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001608 if (any((options.rietveld_url, options.rietveld_email_file,
1609 options.rietveld_fetch, options.rietveld_private_key_file))
1610 and any((options.gerrit_url, options.gerrit_fetch))):
1611 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1612 'allowed')
1613
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001614 if options.rietveld_email and options.rietveld_email_file:
1615 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1616 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001617 if options.rietveld_email_file:
1618 with open(options.rietveld_email_file, "rb") as f:
1619 options.rietveld_email = f.read().strip()
1620
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001621 change_class, files = load_files(options, args)
1622 if not change_class:
1623 parser.error('For unversioned directory, <files> is not optional.')
tobiasjs2836bcf2016-08-16 04:08:16 -07001624 logging.info('Found %d file(s).', len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001625
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001626 rietveld_obj, gerrit_obj = None, None
1627
maruel@chromium.org239f4112011-06-03 20:08:23 +00001628 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001629 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001630 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001631 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1632 options.rietveld_url,
1633 options.rietveld_email,
1634 options.rietveld_private_key_file)
1635 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001636 rietveld_obj = rietveld.CachingRietveld(
1637 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001638 auth_config,
1639 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001640 if options.rietveld_fetch:
1641 assert options.issue
1642 props = rietveld_obj.get_issue_properties(options.issue, False)
1643 options.author = props['owner_email']
1644 options.description = props['description']
1645 logging.info('Got author: "%s"', options.author)
1646 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001647
1648 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001649 assert options.issue and options.patchset
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001650 rietveld_obj = None
1651 gerrit_obj = GerritAccessor(urlparse.urlparse(options.gerrit_url).netloc)
1652 options.author = gerrit_obj.GetChangeOwner(options.issue)
1653 options.description = gerrit_obj.GetChangeDescription(options.issue,
1654 options.patchset)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001655 logging.info('Got author: "%s"', options.author)
1656 logging.info('Got description: """\n%s\n"""', options.description)
1657
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001658 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001659 with canned_check_filter(options.skip_canned):
1660 results = DoPresubmitChecks(
1661 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001662 options.description,
1663 options.root,
1664 files,
1665 options.issue,
1666 options.patchset,
1667 options.author,
1668 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001669 options.commit,
1670 options.verbose,
1671 sys.stdout,
1672 sys.stdin,
1673 options.default_presubmit,
1674 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001675 rietveld_obj,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001676 gerrit_obj,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001677 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001678 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001679 except NonexistantCannedCheckFilter, e:
1680 print >> sys.stderr, (
1681 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1682 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001683 except PresubmitFailure, e:
1684 print >> sys.stderr, e
1685 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001686 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001687
1688
1689if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001690 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001691 try:
1692 sys.exit(main())
1693 except KeyboardInterrupt:
1694 sys.stderr.write('interrupted\n')
sergiybf8a3b382016-07-05 11:21:30 -07001695 sys.exit(2)