blob: 07cf7b24b088993f31f390a84bdafff4cc0acf0d [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
maruel@chromium.org3bbf2942012-01-10 16:52:06 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Enables directory-specific presubmit checks to run at upload and/or commit.
7"""
8
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00009__version__ = '1.8.0'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000010
11# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
12# caching (between all different invocations of presubmit scripts for a given
13# change). We should add it as our presubmit scripts start feeling slow.
14
enne@chromium.orge72c5f52013-04-16 00:36:40 +000015import cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000016import cPickle # Exposed through the API.
17import cStringIO # Exposed through the API.
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +000018import contextlib
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000019import fnmatch
20import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000021import inspect
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +000022import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000023import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000024import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000025import marshal # Exposed through the API.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000026import multiprocessing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000027import optparse
28import os # Somewhat exposed through the API.
29import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000030import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000031import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import sys # Parts exposed through API.
33import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000034import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000035import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000036import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000037import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000038import urllib2 # Exposed through the API.
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000039import urlparse
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000040from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000041
42# Local imports.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
maruel@chromium.org35625c72011-03-23 17:34:02 +000044import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000045import gclient_utils
tandrii@chromium.org015ebae2016-04-25 19:37:22 +000046import gerrit_util
dpranke@chromium.org2a009622011-03-01 02:43:31 +000047import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000048import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000049import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000050import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000051import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052
53
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000054# Ask for feedback only once in program lifetime.
55_ASKED_FOR_FEEDBACK = False
56
57
maruel@chromium.org899e1c12011-04-07 17:03:18 +000058class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000059 pass
60
61
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000062class CommandData(object):
63 def __init__(self, name, cmd, kwargs, message):
64 self.name = name
65 self.cmd = cmd
66 self.kwargs = kwargs
67 self.message = message
68 self.info = None
69
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000070
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000071def normpath(path):
72 '''Version of os.path.normpath that also changes backward slashes to
73 forward slashes when not running on Windows.
74 '''
75 # This is safe to always do because the Windows version of os.path.normpath
76 # will replace forward slashes with backward slashes.
77 path = path.replace(os.sep, '/')
78 return os.path.normpath(path)
79
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000080
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000081def _RightHandSideLinesImpl(affected_files):
82 """Implements RightHandSideLines for InputApi and GclChange."""
83 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000084 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000085 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000086 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000087
88
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000089class PresubmitOutput(object):
90 def __init__(self, input_stream=None, output_stream=None):
91 self.input_stream = input_stream
92 self.output_stream = output_stream
93 self.reviewers = []
94 self.written_output = []
95 self.error_count = 0
96
97 def prompt_yes_no(self, prompt_string):
98 self.write(prompt_string)
99 if self.input_stream:
100 response = self.input_stream.readline().strip().lower()
101 if response not in ('y', 'yes'):
102 self.fail()
103 else:
104 self.fail()
105
106 def fail(self):
107 self.error_count += 1
108
109 def should_continue(self):
110 return not self.error_count
111
112 def write(self, s):
113 self.written_output.append(s)
114 if self.output_stream:
115 self.output_stream.write(s)
116
117 def getvalue(self):
118 return ''.join(self.written_output)
119
120
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000121# Top level object so multiprocessing can pickle
122# Public access through OutputApi object.
123class _PresubmitResult(object):
124 """Base class for result objects."""
125 fatal = False
126 should_prompt = False
127
128 def __init__(self, message, items=None, long_text=''):
129 """
130 message: A short one-line message to indicate errors.
131 items: A list of short strings to indicate where errors occurred.
132 long_text: multi-line text output, e.g. from another tool
133 """
134 self._message = message
135 self._items = items or []
136 if items:
137 self._items = items
138 self._long_text = long_text.rstrip()
139
140 def handle(self, output):
141 output.write(self._message)
142 output.write('\n')
143 for index, item in enumerate(self._items):
144 output.write(' ')
145 # Write separately in case it's unicode.
146 output.write(str(item))
147 if index < len(self._items) - 1:
148 output.write(' \\')
149 output.write('\n')
150 if self._long_text:
151 output.write('\n***************\n')
152 # Write separately in case it's unicode.
153 output.write(self._long_text)
154 output.write('\n***************\n')
155 if self.fatal:
156 output.fail()
157
158
159# Top level object so multiprocessing can pickle
160# Public access through OutputApi object.
161class _PresubmitAddReviewers(_PresubmitResult):
162 """Add some suggested reviewers to the change."""
163 def __init__(self, reviewers):
164 super(_PresubmitAddReviewers, self).__init__('')
165 self.reviewers = reviewers
166
167 def handle(self, output):
168 output.reviewers.extend(self.reviewers)
169
170
171# Top level object so multiprocessing can pickle
172# Public access through OutputApi object.
173class _PresubmitError(_PresubmitResult):
174 """A hard presubmit error."""
175 fatal = True
176
177
178# Top level object so multiprocessing can pickle
179# Public access through OutputApi object.
180class _PresubmitPromptWarning(_PresubmitResult):
181 """An warning that prompts the user if they want to continue."""
182 should_prompt = True
183
184
185# Top level object so multiprocessing can pickle
186# Public access through OutputApi object.
187class _PresubmitNotifyResult(_PresubmitResult):
188 """Just print something to the screen -- but it's not even a warning."""
189 pass
190
191
192# Top level object so multiprocessing can pickle
193# Public access through OutputApi object.
194class _MailTextResult(_PresubmitResult):
195 """A warning that should be included in the review request email."""
196 def __init__(self, *args, **kwargs):
197 super(_MailTextResult, self).__init__()
198 raise NotImplementedError()
199
200
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000201class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000202 """An instance of OutputApi gets passed to presubmit scripts so that they
203 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000204 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000205 PresubmitResult = _PresubmitResult
206 PresubmitAddReviewers = _PresubmitAddReviewers
207 PresubmitError = _PresubmitError
208 PresubmitPromptWarning = _PresubmitPromptWarning
209 PresubmitNotifyResult = _PresubmitNotifyResult
210 MailTextResult = _MailTextResult
211
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000212 def __init__(self, is_committing):
213 self.is_committing = is_committing
214
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000215 def PresubmitPromptOrNotify(self, *args, **kwargs):
216 """Warn the user when uploading, but only notify if committing."""
217 if self.is_committing:
218 return self.PresubmitNotifyResult(*args, **kwargs)
219 return self.PresubmitPromptWarning(*args, **kwargs)
220
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000221
222class InputApi(object):
223 """An instance of this object is passed to presubmit scripts so they can
224 know stuff about the change they're looking at.
225 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000226 # Method could be a function
227 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000228
maruel@chromium.org3410d912009-06-09 20:56:16 +0000229 # File extensions that are considered source files from a style guide
230 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000231 #
232 # Files without an extension aren't included in the list. If you want to
233 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
234 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000235 DEFAULT_WHITE_LIST = (
236 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000237 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
238 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000239 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000240 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000241 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000242 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000243 )
244
245 # Path regexp that should be excluded from being considered containing source
246 # files. Don't modify this list from a presubmit script!
247 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000248 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000249 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000250 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
251 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000252 # Output directories (just in case)
253 r".*\bDebug[\\\/].*",
254 r".*\bRelease[\\\/].*",
255 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000256 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000257 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000258 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000259 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000260 r"(|.*[\\\/])\.git[\\\/].*",
261 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000262 # There is no point in processing a patch file.
263 r".+\.diff$",
264 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000265 )
266
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000267 def __init__(self, change, presubmit_path, is_committing,
tandrii@chromium.org83b1b232016-04-29 16:33:19 +0000268 rietveld_obj, verbose, dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000269 """Builds an InputApi object.
270
271 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000272 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000273 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000274 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000275 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000276 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000277 # Version number of the presubmit_support script.
278 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000279 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000280 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000281 self.rietveld = rietveld_obj
tandrii@chromium.org57bafac2016-04-28 05:09:03 +0000282 self.dry_run = dry_run
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000283 # TBD
284 self.host_url = 'http://codereview.chromium.org'
285 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000286 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000287
288 # We expose various modules and functions as attributes of the input_api
289 # so that presubmit scripts don't have to import them.
290 self.basename = os.path.basename
291 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000292 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000293 self.cStringIO = cStringIO
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000294 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000295 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000296 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000297 self.os_listdir = os.listdir
298 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000299 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000300 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000301 self.pickle = pickle
302 self.marshal = marshal
303 self.re = re
304 self.subprocess = subprocess
305 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000306 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000307 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000308 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000309 self.urllib2 = urllib2
310
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000311 # To easily fork python.
312 self.python_executable = sys.executable
313 self.environ = os.environ
314
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000315 # InputApi.platform is the platform you're currently running on.
316 self.platform = sys.platform
317
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000318 self.cpu_count = multiprocessing.cpu_count()
319
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000320 # this is done here because in RunTests, the current working directory has
321 # changed, which causes Pool() to explode fantastically when run on windows
322 # (because it tries to load the __main__ module, which imports lots of
323 # things relative to the current working directory).
324 self._run_tests_pool = multiprocessing.Pool(self.cpu_count)
325
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000326 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000327 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000328
329 # We carry the canned checks so presubmit scripts can easily use them.
330 self.canned_checks = presubmit_canned_checks
331
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000332 # TODO(dpranke): figure out a list of all approved owners for a repo
333 # in order to be able to handle wildcard OWNERS files?
334 self.owners_db = owners.Database(change.RepositoryRoot(),
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000335 fopen=file, os_path=self.os_path, glob=self.glob)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000336 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000337 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000338
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000339 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000340 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000341 # Access to a protected member _XX of a client class
342 # pylint: disable=W0212
343 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000344 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000345 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
346 for (a, b, header) in cpplint._re_pattern_templates
347 ]
348
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000349 def PresubmitLocalPath(self):
350 """Returns the local path of the presubmit script currently being run.
351
352 This is useful if you don't want to hard-code absolute paths in the
353 presubmit script. For example, It can be used to find another file
354 relative to the PRESUBMIT.py script, so the whole tree can be branched and
355 the presubmit script still works, without editing its content.
356 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000357 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000358
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000359 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000360 """Translate a depot path to a local path (relative to client root).
361
362 Args:
363 Depot path as a string.
364
365 Returns:
366 The local path of the depot path under the user's current client, or None
367 if the file is not mapped.
368
369 Remember to check for the None case and show an appropriate error!
370 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000371 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
372 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000373
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000374 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000375 """Translate a local path to a depot path.
376
377 Args:
378 Local path (relative to current directory, or absolute) as a string.
379
380 Returns:
381 The depot path (SVN URL) of the file if mapped, otherwise None.
382 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000383 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
384 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000385
sail@chromium.org5538e022011-05-12 17:53:16 +0000386 def AffectedFiles(self, include_dirs=False, include_deletes=True,
387 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000388 """Same as input_api.change.AffectedFiles() except only lists files
389 (and optionally directories) in the same directory as the current presubmit
390 script, or subdirectories thereof.
391 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000392 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000393 if len(dir_with_slash) == 1:
394 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000395
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000396 return filter(
397 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000398 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000399
400 def LocalPaths(self, include_dirs=False):
401 """Returns local paths of input_api.AffectedFiles()."""
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000402 paths = [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
403 logging.debug("LocalPaths: %s", paths)
404 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000405
406 def AbsoluteLocalPaths(self, include_dirs=False):
407 """Returns absolute local paths of input_api.AffectedFiles()."""
408 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
409
410 def ServerPaths(self, include_dirs=False):
411 """Returns server paths of input_api.AffectedFiles()."""
412 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
413
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000414 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000415 """Same as input_api.change.AffectedTextFiles() except only lists files
416 in the same directory as the current presubmit script, or subdirectories
417 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000418 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000419 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000420 warn("AffectedTextFiles(include_deletes=%s)"
421 " is deprecated and ignored" % str(include_deletes),
422 category=DeprecationWarning,
423 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000424 return filter(lambda x: x.IsTextFile(),
425 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000426
maruel@chromium.org3410d912009-06-09 20:56:16 +0000427 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
428 """Filters out files that aren't considered "source file".
429
430 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
431 and InputApi.DEFAULT_BLACK_LIST is used respectively.
432
433 The lists will be compiled as regular expression and
434 AffectedFile.LocalPath() needs to pass both list.
435
436 Note: Copy-paste this function to suit your needs or use a lambda function.
437 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000438 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000439 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000440 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000441 if self.re.match(item, local_path):
442 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000443 return True
444 return False
445 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
446 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
447
448 def AffectedSourceFiles(self, source_file):
449 """Filter the list of AffectedTextFiles by the function source_file.
450
451 If source_file is None, InputApi.FilterSourceFile() is used.
452 """
453 if not source_file:
454 source_file = self.FilterSourceFile
455 return filter(source_file, self.AffectedTextFiles())
456
457 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000458 """An iterator over all text lines in "new" version of changed files.
459
460 Only lists lines from new or modified text files in the change that are
461 contained by the directory of the currently executing presubmit script.
462
463 This is useful for doing line-by-line regex checks, like checking for
464 trailing whitespace.
465
466 Yields:
467 a 3 tuple:
468 the AffectedFile instance of the current file;
469 integer line number (1-based); and
470 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000471
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000472 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000473 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000474 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000475 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000476
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000477 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000478 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000479
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000480 Deny reading anything outside the repository.
481 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000482 if isinstance(file_item, AffectedFile):
483 file_item = file_item.AbsoluteLocalPath()
484 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000485 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000486 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000487
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000488 @property
489 def tbr(self):
490 """Returns if a change is TBR'ed."""
491 return 'TBR' in self.change.tags
492
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000493 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000494 tests = []
495 msgs = []
496 for t in tests_mix:
497 if isinstance(t, OutputApi.PresubmitResult):
498 msgs.append(t)
499 else:
500 assert issubclass(t.message, _PresubmitResult)
501 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000502 if self.verbose:
503 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000504 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000505 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000506 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000507 else:
508 msgs.extend(map(CallCommand, tests))
509 return [m for m in msgs if m]
510
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000511
nick@chromium.orgff526192013-06-10 19:30:26 +0000512class _DiffCache(object):
513 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000514 def __init__(self, upstream=None):
515 """Stores the upstream revision against which all diffs will be computed."""
516 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000517
518 def GetDiff(self, path, local_root):
519 """Get the diff for a particular path."""
520 raise NotImplementedError()
521
522
523class _SvnDiffCache(_DiffCache):
524 """DiffCache implementation for subversion."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000525 def __init__(self, *args, **kwargs):
526 super(_SvnDiffCache, self).__init__(*args, **kwargs)
nick@chromium.orgff526192013-06-10 19:30:26 +0000527 self._diffs_by_file = {}
528
529 def GetDiff(self, path, local_root):
530 if path not in self._diffs_by_file:
531 self._diffs_by_file[path] = scm.SVN.GenerateDiff([path], local_root,
532 False, None)
533 return self._diffs_by_file[path]
534
535
536class _GitDiffCache(_DiffCache):
537 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000538 def __init__(self, upstream):
539 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000540 self._diffs_by_file = None
541
542 def GetDiff(self, path, local_root):
543 if not self._diffs_by_file:
544 # Compute a single diff for all files and parse the output; should
545 # with git this is much faster than computing one diff for each file.
546 diffs = {}
547
548 # Don't specify any filenames below, because there are command line length
549 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000550 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
551 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000552
553 # This regex matches the path twice, separated by a space. Note that
554 # filename itself may contain spaces.
555 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
556 current_diff = []
557 keep_line_endings = True
558 for x in unified_diff.splitlines(keep_line_endings):
559 match = file_marker.match(x)
560 if match:
561 # Marks the start of a new per-file section.
562 diffs[match.group('filename')] = current_diff = [x]
563 elif x.startswith('diff --git'):
564 raise PresubmitFailure('Unexpected diff line: %s' % x)
565 else:
566 current_diff.append(x)
567
568 self._diffs_by_file = dict(
569 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
570
571 if path not in self._diffs_by_file:
572 raise PresubmitFailure(
573 'Unified diff did not contain entry for file %s' % path)
574
575 return self._diffs_by_file[path]
576
577
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000578class AffectedFile(object):
579 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000580
581 DIFF_CACHE = _DiffCache
582
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000583 # Method could be a function
584 # pylint: disable=R0201
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000585 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000586 self._path = path
587 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000588 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000589 self._is_directory = None
590 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000591 self._cached_changed_contents = None
592 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000593 self._diff_cache = diff_cache
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000594 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000595
596 def ServerPath(self):
597 """Returns a path string that identifies the file in the SCM system.
598
599 Returns the empty string if the file does not exist in SCM.
600 """
nick@chromium.orgff526192013-06-10 19:30:26 +0000601 return ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000602
603 def LocalPath(self):
604 """Returns the path of this file on the local disk relative to client root.
605 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000606 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000607
608 def AbsoluteLocalPath(self):
609 """Returns the absolute path of this file on the local disk.
610 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000611 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000612
613 def IsDirectory(self):
614 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000615 if self._is_directory is None:
616 path = self.AbsoluteLocalPath()
617 self._is_directory = (os.path.exists(path) and
618 os.path.isdir(path))
619 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000620
621 def Action(self):
622 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000623 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
624 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000625 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000626
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000627 def Property(self, property_name):
628 """Returns the specified SCM property of this file, or None if no such
629 property.
630 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000631 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000632
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000633 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000634 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000635
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000636 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000637 raise NotImplementedError() # Implement when needed
638
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000639 def NewContents(self):
640 """Returns an iterator over the lines in the new version of file.
641
642 The new version is the file in the user's workspace, i.e. the "right hand
643 side".
644
645 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000646 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000647 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000648 if self._cached_new_contents is None:
649 self._cached_new_contents = []
650 if not self.IsDirectory():
651 try:
652 self._cached_new_contents = gclient_utils.FileRead(
653 self.AbsoluteLocalPath(), 'rU').splitlines()
654 except IOError:
655 pass # File not found? That's fine; maybe it was deleted.
656 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000657
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000658 def ChangedContents(self):
659 """Returns a list of tuples (line number, line text) of all new lines.
660
661 This relies on the scm diff output describing each changed code section
662 with a line of the form
663
664 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
665 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000666 if self._cached_changed_contents is not None:
667 return self._cached_changed_contents[:]
668 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000669 line_num = 0
670
671 if self.IsDirectory():
672 return []
673
674 for line in self.GenerateScmDiff().splitlines():
675 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
676 if m:
677 line_num = int(m.groups(1)[0])
678 continue
679 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000680 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000681 if not line.startswith('-'):
682 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000683 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000684
maruel@chromium.org5de13972009-06-10 18:16:06 +0000685 def __str__(self):
686 return self.LocalPath()
687
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000688 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000689 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000690
maruel@chromium.org58407af2011-04-12 23:15:57 +0000691
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000692class SvnAffectedFile(AffectedFile):
693 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000694 # Method 'NNN' is abstract in class 'NNN' but is not overridden
695 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000696
nick@chromium.orgff526192013-06-10 19:30:26 +0000697 DIFF_CACHE = _SvnDiffCache
698
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000699 def __init__(self, *args, **kwargs):
700 AffectedFile.__init__(self, *args, **kwargs)
701 self._server_path = None
702 self._is_text_file = None
703
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000704 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000705 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000706 self._server_path = scm.SVN.CaptureLocalInfo(
707 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000708 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000709
710 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000711 if self._is_directory is None:
712 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000713 if os.path.exists(path):
714 # Retrieve directly from the file system; it is much faster than
715 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000716 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000717 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000718 self._is_directory = scm.SVN.CaptureLocalInfo(
719 [self.LocalPath()], self._local_root
720 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000721 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000722
723 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000724 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000725 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000726 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000727 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000728
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000729 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000730 if self._is_text_file is None:
731 if self.Action() == 'D':
732 # A deleted file is not a text file.
733 self._is_text_file = False
734 elif self.IsDirectory():
735 self._is_text_file = False
736 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000737 mime_type = scm.SVN.GetFileProperty(
738 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000739 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
740 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000741
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000742
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000743class GitAffectedFile(AffectedFile):
744 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000745 # Method 'NNN' is abstract in class 'NNN' but is not overridden
746 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000747
nick@chromium.orgff526192013-06-10 19:30:26 +0000748 DIFF_CACHE = _GitDiffCache
749
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000750 def __init__(self, *args, **kwargs):
751 AffectedFile.__init__(self, *args, **kwargs)
752 self._server_path = None
753 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000754
755 def ServerPath(self):
756 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000757 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000758 return self._server_path
759
760 def IsDirectory(self):
761 if self._is_directory is None:
762 path = self.AbsoluteLocalPath()
763 if os.path.exists(path):
764 # Retrieve directly from the file system; it is much faster than
765 # querying subversion, especially on Windows.
766 self._is_directory = os.path.isdir(path)
767 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000768 self._is_directory = False
769 return self._is_directory
770
771 def Property(self, property_name):
772 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000773 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000774 return self._properties[property_name]
775
776 def IsTextFile(self):
777 if self._is_text_file is None:
778 if self.Action() == 'D':
779 # A deleted file is not a text file.
780 self._is_text_file = False
781 elif self.IsDirectory():
782 self._is_text_file = False
783 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000784 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
785 return self._is_text_file
786
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000787
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000788class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000789 """Describe a change.
790
791 Used directly by the presubmit scripts to query the current change being
792 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000793
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000794 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000795 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000796 self.KEY: equivalent to tags['KEY']
797 """
798
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000799 _AFFECTED_FILES = AffectedFile
800
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000801 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000802 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000803 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000804 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000805
maruel@chromium.org58407af2011-04-12 23:15:57 +0000806 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000807 self, name, description, local_root, files, issue, patchset, author,
808 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000809 if files is None:
810 files = []
811 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000812 # Convert root into an absolute path.
813 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000814 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000815 self.issue = issue
816 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000817 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000818
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000819 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000820 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000821 self._description_without_tags = ''
822 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000823
maruel@chromium.orge085d812011-10-10 19:49:15 +0000824 assert all(
825 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
826
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000827 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000828 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000829 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
830 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000831 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000832
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000833 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000834 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000835 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000836
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000837 def DescriptionText(self):
838 """Returns the user-entered changelist description, minus tags.
839
840 Any line in the user-provided description starting with e.g. "FOO="
841 (whitespace permitted before and around) is considered a tag line. Such
842 lines are stripped out of the description this function returns.
843 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000844 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000845
846 def FullDescriptionText(self):
847 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000848 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000849
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000850 def SetDescriptionText(self, description):
851 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000852
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000853 Also updates the list of tags."""
854 self._full_description = description
855
856 # From the description text, build up a dictionary of key/value pairs
857 # plus the description minus all key/value or "tag" lines.
858 description_without_tags = []
859 self.tags = {}
860 for line in self._full_description.splitlines():
861 m = self.TAG_LINE_RE.match(line)
862 if m:
863 self.tags[m.group('key')] = m.group('value')
864 else:
865 description_without_tags.append(line)
866
867 # Change back to text and remove whitespace at end.
868 self._description_without_tags = (
869 '\n'.join(description_without_tags).rstrip())
870
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000871 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000872 """Returns the repository (checkout) root directory for this change,
873 as an absolute path.
874 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000875 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000876
877 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000878 """Return tags directly as attributes on the object."""
879 if not re.match(r"^[A-Z_]*$", attr):
880 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000881 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000882
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000883 def AllFiles(self, root=None):
884 """List all files under source control in the repo."""
885 raise NotImplementedError()
886
sail@chromium.org5538e022011-05-12 17:53:16 +0000887 def AffectedFiles(self, include_dirs=False, include_deletes=True,
888 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000889 """Returns a list of AffectedFile instances for all files in the change.
890
891 Args:
892 include_deletes: If false, deleted files will be filtered out.
893 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000894 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000895
896 Returns:
897 [AffectedFile(path, action), AffectedFile(path, action)]
898 """
899 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000900 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000901 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000902 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000903
sail@chromium.org5538e022011-05-12 17:53:16 +0000904 affected = filter(file_filter, affected)
905
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000906 if include_deletes:
907 return affected
908 else:
909 return filter(lambda x: x.Action() != 'D', affected)
910
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000911 def AffectedTextFiles(self, include_deletes=None):
912 """Return a list of the existing text files in a change."""
913 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000914 warn("AffectedTextFiles(include_deletes=%s)"
915 " is deprecated and ignored" % str(include_deletes),
916 category=DeprecationWarning,
917 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000918 return filter(lambda x: x.IsTextFile(),
919 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000920
921 def LocalPaths(self, include_dirs=False):
922 """Convenience function."""
923 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
924
925 def AbsoluteLocalPaths(self, include_dirs=False):
926 """Convenience function."""
927 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
928
929 def ServerPaths(self, include_dirs=False):
930 """Convenience function."""
931 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
932
933 def RightHandSideLines(self):
934 """An iterator over all text lines in "new" version of changed files.
935
936 Lists lines from new or modified text files in the change.
937
938 This is useful for doing line-by-line regex checks, like checking for
939 trailing whitespace.
940
941 Yields:
942 a 3 tuple:
943 the AffectedFile instance of the current file;
944 integer line number (1-based); and
945 the contents of the line as a string.
946 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000947 return _RightHandSideLinesImpl(
948 x for x in self.AffectedFiles(include_deletes=False)
949 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000950
951
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000952class SvnChange(Change):
953 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000954 scm = 'svn'
955 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000956
957 def _GetChangeLists(self):
958 """Get all change lists."""
959 if self._changelists == None:
960 previous_cwd = os.getcwd()
961 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000962 # Need to import here to avoid circular dependency.
963 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000964 self._changelists = gcl.GetModifiedFiles()
965 os.chdir(previous_cwd)
966 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000967
968 def GetAllModifiedFiles(self):
969 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000970 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000971 all_modified_files = []
972 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000973 all_modified_files.extend(
974 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000975 return all_modified_files
976
977 def GetModifiedFiles(self):
978 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000979 changelists = self._GetChangeLists()
980 return [os.path.join(self.RepositoryRoot(), f[1])
981 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000982
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000983 def AllFiles(self, root=None):
984 """List all files under source control in the repo."""
985 root = root or self.RepositoryRoot()
986 return subprocess.check_output(
987 ['svn', 'ls', '-R', '.'], cwd=root).splitlines()
988
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000989
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000990class GitChange(Change):
991 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000992 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000993
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000994 def AllFiles(self, root=None):
995 """List all files under source control in the repo."""
996 root = root or self.RepositoryRoot()
997 return subprocess.check_output(
998 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
999
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001000
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001001def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001002 """Finds all presubmit files that apply to a given set of source files.
1003
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001004 If inherit-review-settings-ok is present right under root, looks for
1005 PRESUBMIT.py in directories enclosing root.
1006
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001007 Args:
1008 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001009 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010
1011 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001012 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001013 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001014 files = [normpath(os.path.join(root, f)) for f in files]
1015
1016 # List all the individual directories containing files.
1017 directories = set([os.path.dirname(f) for f in files])
1018
1019 # Ignore root if inherit-review-settings-ok is present.
1020 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1021 root = None
1022
1023 # Collect all unique directories that may contain PRESUBMIT.py.
1024 candidates = set()
1025 for directory in directories:
1026 while True:
1027 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001028 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001029 candidates.add(directory)
1030 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001031 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001032 parent_dir = os.path.dirname(directory)
1033 if parent_dir == directory:
1034 # We hit the system root directory.
1035 break
1036 directory = parent_dir
1037
1038 # Look for PRESUBMIT.py in all candidate directories.
1039 results = []
1040 for directory in sorted(list(candidates)):
1041 p = os.path.join(directory, 'PRESUBMIT.py')
1042 if os.path.isfile(p):
1043 results.append(p)
1044
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001045 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001046 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001047
1048
thestig@chromium.orgde243452009-10-06 21:02:56 +00001049class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001050 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001051 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +00001052 """Executes GetPreferredTrySlaves() from a single presubmit script.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001053
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001054 This will soon be deprecated and replaced by GetPreferredTryMasters().
thestig@chromium.orgde243452009-10-06 21:02:56 +00001055
1056 Args:
1057 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +00001058 presubmit_path: Project script to run.
1059 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001060
1061 Return:
1062 A list of try slaves.
1063 """
1064 context = {}
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001065 main_path = os.getcwd()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001066 try:
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001067 os.chdir(os.path.dirname(presubmit_path))
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001068 exec script_text in context
1069 except Exception, e:
1070 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001071 finally:
1072 os.chdir(main_path)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001073
1074 function_name = 'GetPreferredTrySlaves'
1075 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001076 get_preferred_try_slaves = context[function_name]
1077 function_info = inspect.getargspec(get_preferred_try_slaves)
1078 if len(function_info[0]) == 1:
1079 result = get_preferred_try_slaves(project)
1080 elif len(function_info[0]) == 2:
1081 result = get_preferred_try_slaves(project, change)
1082 else:
1083 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +00001084 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001085 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +00001086 'Presubmit functions must return a list, got a %s instead: %s' %
1087 (type(result), str(result)))
1088 for item in result:
stip@chromium.org68e04192013-11-04 22:14:38 +00001089 if isinstance(item, basestring):
1090 # Old-style ['bot'] format.
1091 botname = item
1092 elif isinstance(item, tuple):
1093 # New-style [('bot', set(['tests']))] format.
1094 botname = item[0]
1095 else:
1096 raise PresubmitFailure('PRESUBMIT.py returned invalid tryslave/test'
1097 ' format.')
1098
1099 if botname != botname.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001100 raise PresubmitFailure(
1101 'Try slave names cannot start/end with whitespace')
stip@chromium.org68e04192013-11-04 22:14:38 +00001102 if ',' in botname:
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +00001103 raise PresubmitFailure(
stip@chromium.org68e04192013-11-04 22:14:38 +00001104 'Do not use \',\' separated builder or test names: %s' % botname)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001105 else:
1106 result = []
stip@chromium.org5ca27622013-12-18 17:44:58 +00001107
1108 def valid_oldstyle(result):
1109 return all(isinstance(i, basestring) for i in result)
1110
1111 def valid_newstyle(result):
1112 return (all(isinstance(i, tuple) for i in result) and
1113 all(len(i) == 2 for i in result) and
1114 all(isinstance(i[0], basestring) for i in result) and
1115 all(isinstance(i[1], set) for i in result)
1116 )
1117
1118 # Ensure it's either all old-style or all new-style.
1119 if not valid_oldstyle(result) and not valid_newstyle(result):
1120 raise PresubmitFailure(
1121 'PRESUBMIT.py returned invalid trybot specification!')
1122
thestig@chromium.orgde243452009-10-06 21:02:56 +00001123 return result
1124
1125
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001126class GetTryMastersExecuter(object):
1127 @staticmethod
1128 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1129 """Executes GetPreferredTryMasters() from a single presubmit script.
1130
1131 Args:
1132 script_text: The text of the presubmit script.
1133 presubmit_path: Project script to run.
1134 project: Project name to pass to presubmit script for bot selection.
1135
1136 Return:
1137 A map of try masters to map of builders to set of tests.
1138 """
1139 context = {}
1140 try:
1141 exec script_text in context
1142 except Exception, e:
1143 raise PresubmitFailure('"%s" had an exception.\n%s'
1144 % (presubmit_path, e))
1145
1146 function_name = 'GetPreferredTryMasters'
1147 if function_name not in context:
1148 return {}
1149 get_preferred_try_masters = context[function_name]
1150 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1151 raise PresubmitFailure(
1152 'Expected function "GetPreferredTryMasters" to take two arguments.')
1153 return get_preferred_try_masters(project, change)
1154
1155
rmistry@google.com5626a922015-02-26 14:03:30 +00001156class GetPostUploadExecuter(object):
1157 @staticmethod
1158 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1159 """Executes PostUploadHook() from a single presubmit script.
1160
1161 Args:
1162 script_text: The text of the presubmit script.
1163 presubmit_path: Project script to run.
1164 cl: The Changelist object.
1165 change: The Change object.
1166
1167 Return:
1168 A list of results objects.
1169 """
1170 context = {}
1171 try:
1172 exec script_text in context
1173 except Exception, e:
1174 raise PresubmitFailure('"%s" had an exception.\n%s'
1175 % (presubmit_path, e))
1176
1177 function_name = 'PostUploadHook'
1178 if function_name not in context:
1179 return {}
1180 post_upload_hook = context[function_name]
1181 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1182 raise PresubmitFailure(
1183 'Expected function "PostUploadHook" to take three arguments.')
1184 return post_upload_hook(cl, change, OutputApi(False))
1185
1186
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001187def DoGetTrySlaves(change,
1188 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001189 repository_root,
1190 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +00001191 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001192 verbose,
1193 output_stream):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001194 """Get the list of try servers from the presubmit scripts (deprecated).
thestig@chromium.orgde243452009-10-06 21:02:56 +00001195
1196 Args:
1197 changed_files: List of modified files.
1198 repository_root: The repository root.
1199 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +00001200 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001201 verbose: Prints debug info.
1202 output_stream: A stream to write debug output to.
1203
1204 Return:
1205 List of try slaves
1206 """
1207 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1208 if not presubmit_files and verbose:
mdempsky@chromium.orgd59e7612014-03-05 19:55:56 +00001209 output_stream.write("Warning, no PRESUBMIT.py found.\n")
thestig@chromium.orgde243452009-10-06 21:02:56 +00001210 results = []
1211 executer = GetTrySlavesExecuter()
stip@chromium.org5ca27622013-12-18 17:44:58 +00001212
thestig@chromium.orgde243452009-10-06 21:02:56 +00001213 if default_presubmit:
1214 if verbose:
1215 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001216 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001217 results.extend(executer.ExecPresubmitScript(
1218 default_presubmit, fake_path, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001219 for filename in presubmit_files:
1220 filename = os.path.abspath(filename)
1221 if verbose:
1222 output_stream.write("Running %s\n" % filename)
1223 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001224 presubmit_script = gclient_utils.FileRead(filename, 'rU')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001225 results.extend(executer.ExecPresubmitScript(
1226 presubmit_script, filename, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001227
stip@chromium.org5ca27622013-12-18 17:44:58 +00001228
1229 slave_dict = {}
1230 old_style = filter(lambda x: isinstance(x, basestring), results)
1231 new_style = filter(lambda x: isinstance(x, tuple), results)
1232
1233 for result in new_style:
1234 slave_dict.setdefault(result[0], set()).update(result[1])
1235 slaves = list(slave_dict.items())
1236
1237 slaves.extend(set(old_style))
stip@chromium.org68e04192013-11-04 22:14:38 +00001238
thestig@chromium.orgde243452009-10-06 21:02:56 +00001239 if slaves and verbose:
stip@chromium.org5ca27622013-12-18 17:44:58 +00001240 output_stream.write(', '.join((str(x) for x in slaves)))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001241 output_stream.write('\n')
1242 return slaves
1243
1244
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001245def _MergeMasters(masters1, masters2):
1246 """Merges two master maps. Merges also the tests of each builder."""
1247 result = {}
1248 for (master, builders) in itertools.chain(masters1.iteritems(),
1249 masters2.iteritems()):
1250 new_builders = result.setdefault(master, {})
1251 for (builder, tests) in builders.iteritems():
1252 new_builders.setdefault(builder, set([])).update(tests)
1253 return result
1254
1255
1256def DoGetTryMasters(change,
1257 changed_files,
1258 repository_root,
1259 default_presubmit,
1260 project,
1261 verbose,
1262 output_stream):
1263 """Get the list of try masters from the presubmit scripts.
1264
1265 Args:
1266 changed_files: List of modified files.
1267 repository_root: The repository root.
1268 default_presubmit: A default presubmit script to execute in any case.
1269 project: Optional name of a project used in selecting trybots.
1270 verbose: Prints debug info.
1271 output_stream: A stream to write debug output to.
1272
1273 Return:
1274 Map of try masters to map of builders to set of tests.
1275 """
1276 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1277 if not presubmit_files and verbose:
1278 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1279 results = {}
1280 executer = GetTryMastersExecuter()
1281
1282 if default_presubmit:
1283 if verbose:
1284 output_stream.write("Running default presubmit script.\n")
1285 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1286 results = _MergeMasters(results, executer.ExecPresubmitScript(
1287 default_presubmit, fake_path, project, change))
1288 for filename in presubmit_files:
1289 filename = os.path.abspath(filename)
1290 if verbose:
1291 output_stream.write("Running %s\n" % filename)
1292 # Accept CRLF presubmit script.
1293 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1294 results = _MergeMasters(results, executer.ExecPresubmitScript(
1295 presubmit_script, filename, project, change))
1296
1297 # Make sets to lists again for later JSON serialization.
1298 for builders in results.itervalues():
1299 for builder in builders:
1300 builders[builder] = list(builders[builder])
1301
1302 if results and verbose:
1303 output_stream.write('%s\n' % str(results))
1304 return results
1305
1306
rmistry@google.com5626a922015-02-26 14:03:30 +00001307def DoPostUploadExecuter(change,
1308 cl,
1309 repository_root,
1310 verbose,
1311 output_stream):
1312 """Execute the post upload hook.
1313
1314 Args:
1315 change: The Change object.
1316 cl: The Changelist object.
1317 repository_root: The repository root.
1318 verbose: Prints debug info.
1319 output_stream: A stream to write debug output to.
1320 """
1321 presubmit_files = ListRelevantPresubmitFiles(
1322 change.LocalPaths(), repository_root)
1323 if not presubmit_files and verbose:
1324 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1325 results = []
1326 executer = GetPostUploadExecuter()
1327 # The root presubmit file should be executed after the ones in subdirectories.
1328 # i.e. the specific post upload hooks should run before the general ones.
1329 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1330 presubmit_files.reverse()
1331
1332 for filename in presubmit_files:
1333 filename = os.path.abspath(filename)
1334 if verbose:
1335 output_stream.write("Running %s\n" % filename)
1336 # Accept CRLF presubmit script.
1337 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1338 results.extend(executer.ExecPresubmitScript(
1339 presubmit_script, filename, cl, change))
1340 output_stream.write('\n')
1341 if results:
1342 output_stream.write('** Post Upload Hook Messages **\n')
1343 for result in results:
1344 result.handle(output_stream)
1345 output_stream.write('\n')
1346
1347 return results
1348
1349
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001350class PresubmitExecuter(object):
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001351 def __init__(self, change, committing, rietveld_obj, verbose,
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001352 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001353 """
1354 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001355 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001356 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001357 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001358 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001359 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001360 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001361 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001362 self.verbose = verbose
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001363 self.dry_run = dry_run
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001364
1365 def ExecPresubmitScript(self, script_text, presubmit_path):
1366 """Executes a single presubmit script.
1367
1368 Args:
1369 script_text: The text of the presubmit script.
1370 presubmit_path: The path to the presubmit file (this will be reported via
1371 input_api.PresubmitLocalPath()).
1372
1373 Return:
1374 A list of result objects, empty if no problems.
1375 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001376
chase@chromium.org8e416c82009-10-06 04:30:44 +00001377 # Change to the presubmit file's directory to support local imports.
1378 main_path = os.getcwd()
1379 os.chdir(os.path.dirname(presubmit_path))
1380
1381 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001382 input_api = InputApi(self.change, presubmit_path, self.committing,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001383 self.rietveld, self.verbose,
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001384 dry_run=self.dry_run)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001385 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001386 try:
1387 exec script_text in context
1388 except Exception, e:
1389 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001390
1391 # These function names must change if we make substantial changes to
1392 # the presubmit API that are not backwards compatible.
1393 if self.committing:
1394 function_name = 'CheckChangeOnCommit'
1395 else:
1396 function_name = 'CheckChangeOnUpload'
1397 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001398 context['__args'] = (input_api, OutputApi(self.committing))
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001399 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001400 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001401 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001402 if not (isinstance(result, types.TupleType) or
1403 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001404 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001405 'Presubmit functions must return a tuple or list')
1406 for item in result:
1407 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001408 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001409 'All presubmit results must be of types derived from '
1410 'output_api.PresubmitResult')
1411 else:
1412 result = () # no error since the script doesn't care about current event.
1413
chase@chromium.org8e416c82009-10-06 04:30:44 +00001414 # Return the process to the original working directory.
1415 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001416 return result
1417
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001418
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001419def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001420 committing,
1421 verbose,
1422 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001423 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001424 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001425 may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001426 rietveld_obj,
1427 dry_run=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001428 """Runs all presubmit checks that apply to the files in the change.
1429
1430 This finds all PRESUBMIT.py files in directories enclosing the files in the
1431 change (up to the repository root) and calls the relevant entrypoint function
1432 depending on whether the change is being committed or uploaded.
1433
1434 Prints errors, warnings and notifications. Prompts the user for warnings
1435 when needed.
1436
1437 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001438 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001439 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1440 verbose: Prints debug info.
1441 output_stream: A stream to write output from presubmit tests to.
1442 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001443 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001444 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001445 rietveld_obj: rietveld.Rietveld object.
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001446 dry_run: if true, some Checks will be skipped.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001447
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001448 Warning:
1449 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1450 SHOULD be sys.stdin.
1451
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001452 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001453 A PresubmitOutput object. Use output.should_continue() to figure out
1454 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001455 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001456 old_environ = os.environ
1457 try:
1458 # Make sure python subprocesses won't generate .pyc files.
1459 os.environ = os.environ.copy()
1460 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001461
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001462 output = PresubmitOutput(input_stream, output_stream)
1463 if committing:
1464 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001465 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001466 output.write("Running presubmit upload checks ...\n")
1467 start_time = time.time()
1468 presubmit_files = ListRelevantPresubmitFiles(
1469 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1470 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001471 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001472 results = []
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001473 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose,
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001474 dry_run=dry_run)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001475 if default_presubmit:
1476 if verbose:
1477 output.write("Running default presubmit script.\n")
1478 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1479 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1480 for filename in presubmit_files:
1481 filename = os.path.abspath(filename)
1482 if verbose:
1483 output.write("Running %s\n" % filename)
1484 # Accept CRLF presubmit script.
1485 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1486 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001487
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001488 errors = []
1489 notifications = []
1490 warnings = []
1491 for result in results:
1492 if result.fatal:
1493 errors.append(result)
1494 elif result.should_prompt:
1495 warnings.append(result)
1496 else:
1497 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001498
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001499 output.write('\n')
1500 for name, items in (('Messages', notifications),
1501 ('Warnings', warnings),
1502 ('ERRORS', errors)):
1503 if items:
1504 output.write('** Presubmit %s **\n' % name)
1505 for item in items:
1506 item.handle(output)
1507 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001508
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001509 total_time = time.time() - start_time
1510 if total_time > 1.0:
1511 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001512
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001513 if not errors:
1514 if not warnings:
1515 output.write('Presubmit checks passed.\n')
1516 elif may_prompt:
1517 output.prompt_yes_no('There were presubmit warnings. '
1518 'Are you sure you wish to continue? (y/N): ')
1519 else:
1520 output.fail()
1521
1522 global _ASKED_FOR_FEEDBACK
1523 # Ask for feedback one time out of 5.
1524 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001525 output.write(
1526 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1527 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1528 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001529 _ASKED_FOR_FEEDBACK = True
1530 return output
1531 finally:
1532 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001533
1534
1535def ScanSubDirs(mask, recursive):
1536 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001537 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001538 else:
1539 results = []
1540 for root, dirs, files in os.walk('.'):
1541 if '.svn' in dirs:
1542 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001543 if '.git' in dirs:
1544 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001545 for name in files:
1546 if fnmatch.fnmatch(name, mask):
1547 results.append(os.path.join(root, name))
1548 return results
1549
1550
1551def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001552 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001553 files = []
1554 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001555 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001556 return files
1557
1558
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001559def load_files(options, args):
1560 """Tries to determine the SCM."""
1561 change_scm = scm.determine_scm(options.root)
1562 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001563 if args:
1564 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001565 if change_scm == 'svn':
1566 change_class = SvnChange
1567 if not files:
1568 files = scm.SVN.CaptureStatus([], options.root)
1569 elif change_scm == 'git':
1570 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001571 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001572 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001573 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001574 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001575 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1576 if not files:
1577 return None, None
1578 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001579 return change_class, files
1580
1581
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001582class NonexistantCannedCheckFilter(Exception):
1583 pass
1584
1585
1586@contextlib.contextmanager
1587def canned_check_filter(method_names):
1588 filtered = {}
1589 try:
1590 for method_name in method_names:
1591 if not hasattr(presubmit_canned_checks, method_name):
1592 raise NonexistantCannedCheckFilter(method_name)
1593 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1594 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1595 yield
1596 finally:
1597 for name, method in filtered.iteritems():
1598 setattr(presubmit_canned_checks, name, method)
1599
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001600
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001601def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001602 """Runs an external program, potentially from a child process created by the
1603 multiprocessing module.
1604
1605 multiprocessing needs a top level function with a single argument.
1606 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001607 cmd_data.kwargs['stdout'] = subprocess.PIPE
1608 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1609 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001610 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001611 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001612 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001613 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001614 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001615 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001616 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1617 if code != 0:
1618 return cmd_data.message(
1619 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1620 if cmd_data.info:
1621 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001622
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001623
sbc@chromium.org013731e2015-02-26 18:28:43 +00001624def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001625 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001626 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001627 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001628 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001629 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1630 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001631 parser.add_option("-r", "--recursive", action="store_true",
1632 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001633 parser.add_option("-v", "--verbose", action="count", default=0,
1634 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001635 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001636 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001637 parser.add_option("--description", default='')
1638 parser.add_option("--issue", type='int', default=0)
1639 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001640 parser.add_option("--root", default=os.getcwd(),
1641 help="Search for PRESUBMIT.py up to this directory. "
1642 "If inherit-review-settings-ok is present in this "
1643 "directory, parent directories up to the root file "
1644 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001645 parser.add_option("--upstream",
1646 help="Git only: the base ref or upstream branch against "
1647 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001648 parser.add_option("--default_presubmit")
1649 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001650 parser.add_option("--skip_canned", action='append', default=[],
1651 help="A list of checks to skip which appear in "
1652 "presubmit_canned_checks. Can be provided multiple times "
1653 "to skip multiple canned checks.")
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001654 parser.add_option("--dry_run", action='store_true',
1655 help=optparse.SUPPRESS_HELP)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001656 parser.add_option("--gerrit_url", help=optparse.SUPPRESS_HELP)
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001657 parser.add_option("--gerrit_fetch", action='store_true',
1658 help=optparse.SUPPRESS_HELP)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001659 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1660 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001661 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1662 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001663 # These are for OAuth2 authentication for bots. See also apply_issue.py
1664 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1665 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1666
phajdan.jr@chromium.org2ff30182016-03-23 09:52:51 +00001667 # TODO(phajdan.jr): Update callers and remove obsolete --trybot-json .
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001668 parser.add_option("--trybot-json",
1669 help="Output trybot information to the file specified.")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001670 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001671 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001672 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001673
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001674 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001675 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001676 elif options.verbose:
1677 logging.basicConfig(level=logging.INFO)
1678 else:
1679 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001680
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001681 if (any((options.rietveld_url, options.rietveld_email_file,
1682 options.rietveld_fetch, options.rietveld_private_key_file))
1683 and any((options.gerrit_url, options.gerrit_fetch))):
1684 parser.error('Options for only codereview --rietveld_* or --gerrit_* '
1685 'allowed')
1686
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001687 if options.rietveld_email and options.rietveld_email_file:
1688 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1689 "can be passed to this program.")
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001690 if options.rietveld_email_file:
1691 with open(options.rietveld_email_file, "rb") as f:
1692 options.rietveld_email = f.read().strip()
1693
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001694 change_class, files = load_files(options, args)
1695 if not change_class:
1696 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001697 logging.info('Found %d file(s).' % len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001698
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001699 rietveld_obj = None
maruel@chromium.org239f4112011-06-03 20:08:23 +00001700 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001701 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001702 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001703 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1704 options.rietveld_url,
1705 options.rietveld_email,
1706 options.rietveld_private_key_file)
1707 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001708 rietveld_obj = rietveld.CachingRietveld(
1709 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001710 auth_config,
1711 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001712 if options.rietveld_fetch:
1713 assert options.issue
1714 props = rietveld_obj.get_issue_properties(options.issue, False)
1715 options.author = props['owner_email']
1716 options.description = props['description']
1717 logging.info('Got author: "%s"', options.author)
1718 logging.info('Got description: """\n%s\n"""', options.description)
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001719
1720 if options.gerrit_url and options.gerrit_fetch:
tandrii@chromium.orgb4cd1962016-04-29 16:04:11 +00001721 rietveld_obj = None
tandrii@chromium.org83b1b232016-04-29 16:33:19 +00001722 assert options.issue and options.patchset
1723 props = gerrit_util.GetChangeDetail(
1724 urlparse.urlparse(options.gerrit_url).netloc, str(options.issue),
1725 ['ALL_REVISIONS'])
1726 options.author = props['owner']['email']
1727 for rev, rev_info in props['revisions'].iteritems():
1728 if str(rev_info['_number']) == str(options.patchset):
1729 options.description = gerrit_util.GetChangeDescriptionFromGitiles(
1730 rev_info['fetch']['http']['url'], rev)
1731 break
1732 else:
1733 print >> sys.stderr, ('Patchset %d was not found in Gerrit issue %d' %
1734 options.patchset, options.issue)
1735 return 2
tandrii@chromium.org015ebae2016-04-25 19:37:22 +00001736 logging.info('Got author: "%s"', options.author)
1737 logging.info('Got description: """\n%s\n"""', options.description)
1738
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001739 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001740 with canned_check_filter(options.skip_canned):
1741 results = DoPresubmitChecks(
1742 change_class(options.name,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001743 options.description,
1744 options.root,
1745 files,
1746 options.issue,
1747 options.patchset,
1748 options.author,
1749 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001750 options.commit,
1751 options.verbose,
1752 sys.stdout,
1753 sys.stdin,
1754 options.default_presubmit,
1755 options.may_prompt,
tandrii@chromium.org57bafac2016-04-28 05:09:03 +00001756 rietveld_obj,
1757 options.dry_run)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001758 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001759 except NonexistantCannedCheckFilter, e:
1760 print >> sys.stderr, (
1761 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1762 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001763 except PresubmitFailure, e:
1764 print >> sys.stderr, e
1765 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1766 print >> sys.stderr, 'If all fails, contact maruel@'
1767 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001768
1769
1770if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001771 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001772 try:
1773 sys.exit(main())
1774 except KeyboardInterrupt:
1775 sys.stderr.write('interrupted\n')
1776 sys.exit(1)