blob: 523bb79a1245f4c30534bea2e90ac447dda49aba [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.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000039from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000040
41# Local imports.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000042import auth
maruel@chromium.org35625c72011-03-23 17:34:02 +000043import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000044import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000045import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000046import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000047import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000048import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000049import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050
51
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000052# Ask for feedback only once in program lifetime.
53_ASKED_FOR_FEEDBACK = False
54
55
maruel@chromium.org899e1c12011-04-07 17:03:18 +000056class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000057 pass
58
59
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000060class CommandData(object):
61 def __init__(self, name, cmd, kwargs, message):
62 self.name = name
63 self.cmd = cmd
64 self.kwargs = kwargs
65 self.message = message
66 self.info = None
67
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000068
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000069def normpath(path):
70 '''Version of os.path.normpath that also changes backward slashes to
71 forward slashes when not running on Windows.
72 '''
73 # This is safe to always do because the Windows version of os.path.normpath
74 # will replace forward slashes with backward slashes.
75 path = path.replace(os.sep, '/')
76 return os.path.normpath(path)
77
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000078
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000079def _RightHandSideLinesImpl(affected_files):
80 """Implements RightHandSideLines for InputApi and GclChange."""
81 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000082 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000083 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000084 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000085
86
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000087class PresubmitOutput(object):
88 def __init__(self, input_stream=None, output_stream=None):
89 self.input_stream = input_stream
90 self.output_stream = output_stream
91 self.reviewers = []
92 self.written_output = []
93 self.error_count = 0
94
95 def prompt_yes_no(self, prompt_string):
96 self.write(prompt_string)
97 if self.input_stream:
98 response = self.input_stream.readline().strip().lower()
99 if response not in ('y', 'yes'):
100 self.fail()
101 else:
102 self.fail()
103
104 def fail(self):
105 self.error_count += 1
106
107 def should_continue(self):
108 return not self.error_count
109
110 def write(self, s):
111 self.written_output.append(s)
112 if self.output_stream:
113 self.output_stream.write(s)
114
115 def getvalue(self):
116 return ''.join(self.written_output)
117
118
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000119# Top level object so multiprocessing can pickle
120# Public access through OutputApi object.
121class _PresubmitResult(object):
122 """Base class for result objects."""
123 fatal = False
124 should_prompt = False
125
126 def __init__(self, message, items=None, long_text=''):
127 """
128 message: A short one-line message to indicate errors.
129 items: A list of short strings to indicate where errors occurred.
130 long_text: multi-line text output, e.g. from another tool
131 """
132 self._message = message
133 self._items = items or []
134 if items:
135 self._items = items
136 self._long_text = long_text.rstrip()
137
138 def handle(self, output):
139 output.write(self._message)
140 output.write('\n')
141 for index, item in enumerate(self._items):
142 output.write(' ')
143 # Write separately in case it's unicode.
144 output.write(str(item))
145 if index < len(self._items) - 1:
146 output.write(' \\')
147 output.write('\n')
148 if self._long_text:
149 output.write('\n***************\n')
150 # Write separately in case it's unicode.
151 output.write(self._long_text)
152 output.write('\n***************\n')
153 if self.fatal:
154 output.fail()
155
156
157# Top level object so multiprocessing can pickle
158# Public access through OutputApi object.
159class _PresubmitAddReviewers(_PresubmitResult):
160 """Add some suggested reviewers to the change."""
161 def __init__(self, reviewers):
162 super(_PresubmitAddReviewers, self).__init__('')
163 self.reviewers = reviewers
164
165 def handle(self, output):
166 output.reviewers.extend(self.reviewers)
167
168
169# Top level object so multiprocessing can pickle
170# Public access through OutputApi object.
171class _PresubmitError(_PresubmitResult):
172 """A hard presubmit error."""
173 fatal = True
174
175
176# Top level object so multiprocessing can pickle
177# Public access through OutputApi object.
178class _PresubmitPromptWarning(_PresubmitResult):
179 """An warning that prompts the user if they want to continue."""
180 should_prompt = True
181
182
183# Top level object so multiprocessing can pickle
184# Public access through OutputApi object.
185class _PresubmitNotifyResult(_PresubmitResult):
186 """Just print something to the screen -- but it's not even a warning."""
187 pass
188
189
190# Top level object so multiprocessing can pickle
191# Public access through OutputApi object.
192class _MailTextResult(_PresubmitResult):
193 """A warning that should be included in the review request email."""
194 def __init__(self, *args, **kwargs):
195 super(_MailTextResult, self).__init__()
196 raise NotImplementedError()
197
198
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000199class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000200 """An instance of OutputApi gets passed to presubmit scripts so that they
201 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000202 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000203 PresubmitResult = _PresubmitResult
204 PresubmitAddReviewers = _PresubmitAddReviewers
205 PresubmitError = _PresubmitError
206 PresubmitPromptWarning = _PresubmitPromptWarning
207 PresubmitNotifyResult = _PresubmitNotifyResult
208 MailTextResult = _MailTextResult
209
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000210 def __init__(self, is_committing):
211 self.is_committing = is_committing
212
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000213 def PresubmitPromptOrNotify(self, *args, **kwargs):
214 """Warn the user when uploading, but only notify if committing."""
215 if self.is_committing:
216 return self.PresubmitNotifyResult(*args, **kwargs)
217 return self.PresubmitPromptWarning(*args, **kwargs)
218
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000219
220class InputApi(object):
221 """An instance of this object is passed to presubmit scripts so they can
222 know stuff about the change they're looking at.
223 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000224 # Method could be a function
225 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000226
maruel@chromium.org3410d912009-06-09 20:56:16 +0000227 # File extensions that are considered source files from a style guide
228 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000229 #
230 # Files without an extension aren't included in the list. If you want to
231 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
232 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000233 DEFAULT_WHITE_LIST = (
234 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000235 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
236 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000237 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000238 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000239 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000240 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000241 )
242
243 # Path regexp that should be excluded from being considered containing source
244 # files. Don't modify this list from a presubmit script!
245 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000246 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000247 r".*\bexperimental[\\\/].*",
248 r".*\bthird_party[\\\/].*",
249 # Output directories (just in case)
250 r".*\bDebug[\\\/].*",
251 r".*\bRelease[\\\/].*",
252 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000253 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000254 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000255 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000256 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000257 r"(|.*[\\\/])\.git[\\\/].*",
258 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000259 # There is no point in processing a patch file.
260 r".+\.diff$",
261 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000262 )
263
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000264 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000265 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000266 """Builds an InputApi object.
267
268 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000269 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000270 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000271 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000272 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000273 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000274 # Version number of the presubmit_support script.
275 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000276 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000277 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000278 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000279 # TBD
280 self.host_url = 'http://codereview.chromium.org'
281 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000282 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000283
284 # We expose various modules and functions as attributes of the input_api
285 # so that presubmit scripts don't have to import them.
286 self.basename = os.path.basename
287 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000288 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000289 self.cStringIO = cStringIO
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000290 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000291 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000292 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000293 self.os_listdir = os.listdir
294 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000295 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000296 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000297 self.pickle = pickle
298 self.marshal = marshal
299 self.re = re
300 self.subprocess = subprocess
301 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000302 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000303 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000304 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000305 self.urllib2 = urllib2
306
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000307 # To easily fork python.
308 self.python_executable = sys.executable
309 self.environ = os.environ
310
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000311 # InputApi.platform is the platform you're currently running on.
312 self.platform = sys.platform
313
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000314 self.cpu_count = multiprocessing.cpu_count()
315
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000316 # this is done here because in RunTests, the current working directory has
317 # changed, which causes Pool() to explode fantastically when run on windows
318 # (because it tries to load the __main__ module, which imports lots of
319 # things relative to the current working directory).
320 self._run_tests_pool = multiprocessing.Pool(self.cpu_count)
321
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000322 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000323 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000324
325 # We carry the canned checks so presubmit scripts can easily use them.
326 self.canned_checks = presubmit_canned_checks
327
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000328 # TODO(dpranke): figure out a list of all approved owners for a repo
329 # in order to be able to handle wildcard OWNERS files?
330 self.owners_db = owners.Database(change.RepositoryRoot(),
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000331 fopen=file, os_path=self.os_path, glob=self.glob)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000332 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000333 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000334
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000335 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000336 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000337 # Access to a protected member _XX of a client class
338 # pylint: disable=W0212
339 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000340 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000341 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
342 for (a, b, header) in cpplint._re_pattern_templates
343 ]
344
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000345 def PresubmitLocalPath(self):
346 """Returns the local path of the presubmit script currently being run.
347
348 This is useful if you don't want to hard-code absolute paths in the
349 presubmit script. For example, It can be used to find another file
350 relative to the PRESUBMIT.py script, so the whole tree can be branched and
351 the presubmit script still works, without editing its content.
352 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000353 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000354
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000355 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000356 """Translate a depot path to a local path (relative to client root).
357
358 Args:
359 Depot path as a string.
360
361 Returns:
362 The local path of the depot path under the user's current client, or None
363 if the file is not mapped.
364
365 Remember to check for the None case and show an appropriate error!
366 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000367 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
368 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000369
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000370 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000371 """Translate a local path to a depot path.
372
373 Args:
374 Local path (relative to current directory, or absolute) as a string.
375
376 Returns:
377 The depot path (SVN URL) of the file if mapped, otherwise None.
378 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000379 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
380 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000381
sail@chromium.org5538e022011-05-12 17:53:16 +0000382 def AffectedFiles(self, include_dirs=False, include_deletes=True,
383 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000384 """Same as input_api.change.AffectedFiles() except only lists files
385 (and optionally directories) in the same directory as the current presubmit
386 script, or subdirectories thereof.
387 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000388 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000389 if len(dir_with_slash) == 1:
390 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000391
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000392 return filter(
393 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000394 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000395
396 def LocalPaths(self, include_dirs=False):
397 """Returns local paths of input_api.AffectedFiles()."""
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000398 paths = [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
399 logging.debug("LocalPaths: %s", paths)
400 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000401
402 def AbsoluteLocalPaths(self, include_dirs=False):
403 """Returns absolute local paths of input_api.AffectedFiles()."""
404 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
405
406 def ServerPaths(self, include_dirs=False):
407 """Returns server paths of input_api.AffectedFiles()."""
408 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
409
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000410 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000411 """Same as input_api.change.AffectedTextFiles() except only lists files
412 in the same directory as the current presubmit script, or subdirectories
413 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000414 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000415 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000416 warn("AffectedTextFiles(include_deletes=%s)"
417 " is deprecated and ignored" % str(include_deletes),
418 category=DeprecationWarning,
419 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000420 return filter(lambda x: x.IsTextFile(),
421 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000422
maruel@chromium.org3410d912009-06-09 20:56:16 +0000423 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
424 """Filters out files that aren't considered "source file".
425
426 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
427 and InputApi.DEFAULT_BLACK_LIST is used respectively.
428
429 The lists will be compiled as regular expression and
430 AffectedFile.LocalPath() needs to pass both list.
431
432 Note: Copy-paste this function to suit your needs or use a lambda function.
433 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000434 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000435 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000436 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000437 if self.re.match(item, local_path):
438 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000439 return True
440 return False
441 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
442 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
443
444 def AffectedSourceFiles(self, source_file):
445 """Filter the list of AffectedTextFiles by the function source_file.
446
447 If source_file is None, InputApi.FilterSourceFile() is used.
448 """
449 if not source_file:
450 source_file = self.FilterSourceFile
451 return filter(source_file, self.AffectedTextFiles())
452
453 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000454 """An iterator over all text lines in "new" version of changed files.
455
456 Only lists lines from new or modified text files in the change that are
457 contained by the directory of the currently executing presubmit script.
458
459 This is useful for doing line-by-line regex checks, like checking for
460 trailing whitespace.
461
462 Yields:
463 a 3 tuple:
464 the AffectedFile instance of the current file;
465 integer line number (1-based); and
466 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000467
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000468 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000469 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000470 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000471 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000472
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000473 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000474 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000475
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000476 Deny reading anything outside the repository.
477 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000478 if isinstance(file_item, AffectedFile):
479 file_item = file_item.AbsoluteLocalPath()
480 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000481 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000482 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000483
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000484 @property
485 def tbr(self):
486 """Returns if a change is TBR'ed."""
487 return 'TBR' in self.change.tags
488
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000489 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000490 tests = []
491 msgs = []
492 for t in tests_mix:
493 if isinstance(t, OutputApi.PresubmitResult):
494 msgs.append(t)
495 else:
496 assert issubclass(t.message, _PresubmitResult)
497 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000498 if self.verbose:
499 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000500 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000501 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000502 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000503 else:
504 msgs.extend(map(CallCommand, tests))
505 return [m for m in msgs if m]
506
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000507
nick@chromium.orgff526192013-06-10 19:30:26 +0000508class _DiffCache(object):
509 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000510 def __init__(self, upstream=None):
511 """Stores the upstream revision against which all diffs will be computed."""
512 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000513
514 def GetDiff(self, path, local_root):
515 """Get the diff for a particular path."""
516 raise NotImplementedError()
517
518
519class _SvnDiffCache(_DiffCache):
520 """DiffCache implementation for subversion."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000521 def __init__(self, *args, **kwargs):
522 super(_SvnDiffCache, self).__init__(*args, **kwargs)
nick@chromium.orgff526192013-06-10 19:30:26 +0000523 self._diffs_by_file = {}
524
525 def GetDiff(self, path, local_root):
526 if path not in self._diffs_by_file:
527 self._diffs_by_file[path] = scm.SVN.GenerateDiff([path], local_root,
528 False, None)
529 return self._diffs_by_file[path]
530
531
532class _GitDiffCache(_DiffCache):
533 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000534 def __init__(self, upstream):
535 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000536 self._diffs_by_file = None
537
538 def GetDiff(self, path, local_root):
539 if not self._diffs_by_file:
540 # Compute a single diff for all files and parse the output; should
541 # with git this is much faster than computing one diff for each file.
542 diffs = {}
543
544 # Don't specify any filenames below, because there are command line length
545 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000546 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
547 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000548
549 # This regex matches the path twice, separated by a space. Note that
550 # filename itself may contain spaces.
551 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
552 current_diff = []
553 keep_line_endings = True
554 for x in unified_diff.splitlines(keep_line_endings):
555 match = file_marker.match(x)
556 if match:
557 # Marks the start of a new per-file section.
558 diffs[match.group('filename')] = current_diff = [x]
559 elif x.startswith('diff --git'):
560 raise PresubmitFailure('Unexpected diff line: %s' % x)
561 else:
562 current_diff.append(x)
563
564 self._diffs_by_file = dict(
565 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
566
567 if path not in self._diffs_by_file:
568 raise PresubmitFailure(
569 'Unified diff did not contain entry for file %s' % path)
570
571 return self._diffs_by_file[path]
572
573
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000574class AffectedFile(object):
575 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000576
577 DIFF_CACHE = _DiffCache
578
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000579 # Method could be a function
580 # pylint: disable=R0201
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000581 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000582 self._path = path
583 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000584 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000585 self._is_directory = None
586 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000587 self._cached_changed_contents = None
588 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000589 self._diff_cache = diff_cache
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000590 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000591
592 def ServerPath(self):
593 """Returns a path string that identifies the file in the SCM system.
594
595 Returns the empty string if the file does not exist in SCM.
596 """
nick@chromium.orgff526192013-06-10 19:30:26 +0000597 return ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000598
599 def LocalPath(self):
600 """Returns the path of this file on the local disk relative to client root.
601 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000602 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000603
604 def AbsoluteLocalPath(self):
605 """Returns the absolute path of this file on the local disk.
606 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000607 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000608
609 def IsDirectory(self):
610 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000611 if self._is_directory is None:
612 path = self.AbsoluteLocalPath()
613 self._is_directory = (os.path.exists(path) and
614 os.path.isdir(path))
615 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000616
617 def Action(self):
618 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000619 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
620 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000621 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000622
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000623 def Property(self, property_name):
624 """Returns the specified SCM property of this file, or None if no such
625 property.
626 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000627 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000628
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000629 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000630 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000631
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000632 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000633 raise NotImplementedError() # Implement when needed
634
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000635 def NewContents(self):
636 """Returns an iterator over the lines in the new version of file.
637
638 The new version is the file in the user's workspace, i.e. the "right hand
639 side".
640
641 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000642 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000643 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000644 if self._cached_new_contents is None:
645 self._cached_new_contents = []
646 if not self.IsDirectory():
647 try:
648 self._cached_new_contents = gclient_utils.FileRead(
649 self.AbsoluteLocalPath(), 'rU').splitlines()
650 except IOError:
651 pass # File not found? That's fine; maybe it was deleted.
652 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000653
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000654 def ChangedContents(self):
655 """Returns a list of tuples (line number, line text) of all new lines.
656
657 This relies on the scm diff output describing each changed code section
658 with a line of the form
659
660 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
661 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000662 if self._cached_changed_contents is not None:
663 return self._cached_changed_contents[:]
664 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000665 line_num = 0
666
667 if self.IsDirectory():
668 return []
669
670 for line in self.GenerateScmDiff().splitlines():
671 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
672 if m:
673 line_num = int(m.groups(1)[0])
674 continue
675 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000676 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000677 if not line.startswith('-'):
678 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000679 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000680
maruel@chromium.org5de13972009-06-10 18:16:06 +0000681 def __str__(self):
682 return self.LocalPath()
683
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000684 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000685 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000686
maruel@chromium.org58407af2011-04-12 23:15:57 +0000687
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000688class SvnAffectedFile(AffectedFile):
689 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000690 # Method 'NNN' is abstract in class 'NNN' but is not overridden
691 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000692
nick@chromium.orgff526192013-06-10 19:30:26 +0000693 DIFF_CACHE = _SvnDiffCache
694
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000695 def __init__(self, *args, **kwargs):
696 AffectedFile.__init__(self, *args, **kwargs)
697 self._server_path = None
698 self._is_text_file = None
699
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000700 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000701 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000702 self._server_path = scm.SVN.CaptureLocalInfo(
703 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000704 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000705
706 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000707 if self._is_directory is None:
708 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000709 if os.path.exists(path):
710 # Retrieve directly from the file system; it is much faster than
711 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000712 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000713 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000714 self._is_directory = scm.SVN.CaptureLocalInfo(
715 [self.LocalPath()], self._local_root
716 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000717 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000718
719 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000720 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000721 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000722 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000723 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000724
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000725 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000726 if self._is_text_file is None:
727 if self.Action() == 'D':
728 # A deleted file is not a text file.
729 self._is_text_file = False
730 elif self.IsDirectory():
731 self._is_text_file = False
732 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000733 mime_type = scm.SVN.GetFileProperty(
734 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000735 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
736 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000737
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000738
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000739class GitAffectedFile(AffectedFile):
740 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000741 # Method 'NNN' is abstract in class 'NNN' but is not overridden
742 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000743
nick@chromium.orgff526192013-06-10 19:30:26 +0000744 DIFF_CACHE = _GitDiffCache
745
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000746 def __init__(self, *args, **kwargs):
747 AffectedFile.__init__(self, *args, **kwargs)
748 self._server_path = None
749 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000750
751 def ServerPath(self):
752 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000753 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000754 return self._server_path
755
756 def IsDirectory(self):
757 if self._is_directory is None:
758 path = self.AbsoluteLocalPath()
759 if os.path.exists(path):
760 # Retrieve directly from the file system; it is much faster than
761 # querying subversion, especially on Windows.
762 self._is_directory = os.path.isdir(path)
763 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000764 self._is_directory = False
765 return self._is_directory
766
767 def Property(self, property_name):
768 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000769 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000770 return self._properties[property_name]
771
772 def IsTextFile(self):
773 if self._is_text_file is None:
774 if self.Action() == 'D':
775 # A deleted file is not a text file.
776 self._is_text_file = False
777 elif self.IsDirectory():
778 self._is_text_file = False
779 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000780 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
781 return self._is_text_file
782
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000783
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000784class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000785 """Describe a change.
786
787 Used directly by the presubmit scripts to query the current change being
788 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000789
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000790 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000791 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000792 self.KEY: equivalent to tags['KEY']
793 """
794
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000795 _AFFECTED_FILES = AffectedFile
796
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000797 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000798 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000799 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000800 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000801
maruel@chromium.org58407af2011-04-12 23:15:57 +0000802 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000803 self, name, description, local_root, files, issue, patchset, author,
804 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000805 if files is None:
806 files = []
807 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000808 # Convert root into an absolute path.
809 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000810 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000811 self.issue = issue
812 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000813 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000814
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000815 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000816 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000817 self._description_without_tags = ''
818 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000819
maruel@chromium.orge085d812011-10-10 19:49:15 +0000820 assert all(
821 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
822
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000823 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000824 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000825 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
826 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000827 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000828
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000829 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000830 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000831 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000832
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000833 def DescriptionText(self):
834 """Returns the user-entered changelist description, minus tags.
835
836 Any line in the user-provided description starting with e.g. "FOO="
837 (whitespace permitted before and around) is considered a tag line. Such
838 lines are stripped out of the description this function returns.
839 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000840 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000841
842 def FullDescriptionText(self):
843 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000844 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000845
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000846 def SetDescriptionText(self, description):
847 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000848
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000849 Also updates the list of tags."""
850 self._full_description = description
851
852 # From the description text, build up a dictionary of key/value pairs
853 # plus the description minus all key/value or "tag" lines.
854 description_without_tags = []
855 self.tags = {}
856 for line in self._full_description.splitlines():
857 m = self.TAG_LINE_RE.match(line)
858 if m:
859 self.tags[m.group('key')] = m.group('value')
860 else:
861 description_without_tags.append(line)
862
863 # Change back to text and remove whitespace at end.
864 self._description_without_tags = (
865 '\n'.join(description_without_tags).rstrip())
866
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000867 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000868 """Returns the repository (checkout) root directory for this change,
869 as an absolute path.
870 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000871 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000872
873 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000874 """Return tags directly as attributes on the object."""
875 if not re.match(r"^[A-Z_]*$", attr):
876 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000877 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000878
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000879 def AllFiles(self, root=None):
880 """List all files under source control in the repo."""
881 raise NotImplementedError()
882
sail@chromium.org5538e022011-05-12 17:53:16 +0000883 def AffectedFiles(self, include_dirs=False, include_deletes=True,
884 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000885 """Returns a list of AffectedFile instances for all files in the change.
886
887 Args:
888 include_deletes: If false, deleted files will be filtered out.
889 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000890 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000891
892 Returns:
893 [AffectedFile(path, action), AffectedFile(path, action)]
894 """
895 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000896 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000897 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000898 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000899
sail@chromium.org5538e022011-05-12 17:53:16 +0000900 affected = filter(file_filter, affected)
901
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000902 if include_deletes:
903 return affected
904 else:
905 return filter(lambda x: x.Action() != 'D', affected)
906
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000907 def AffectedTextFiles(self, include_deletes=None):
908 """Return a list of the existing text files in a change."""
909 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000910 warn("AffectedTextFiles(include_deletes=%s)"
911 " is deprecated and ignored" % str(include_deletes),
912 category=DeprecationWarning,
913 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000914 return filter(lambda x: x.IsTextFile(),
915 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000916
917 def LocalPaths(self, include_dirs=False):
918 """Convenience function."""
919 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
920
921 def AbsoluteLocalPaths(self, include_dirs=False):
922 """Convenience function."""
923 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
924
925 def ServerPaths(self, include_dirs=False):
926 """Convenience function."""
927 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
928
929 def RightHandSideLines(self):
930 """An iterator over all text lines in "new" version of changed files.
931
932 Lists lines from new or modified text files in the change.
933
934 This is useful for doing line-by-line regex checks, like checking for
935 trailing whitespace.
936
937 Yields:
938 a 3 tuple:
939 the AffectedFile instance of the current file;
940 integer line number (1-based); and
941 the contents of the line as a string.
942 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000943 return _RightHandSideLinesImpl(
944 x for x in self.AffectedFiles(include_deletes=False)
945 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000946
947
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000948class SvnChange(Change):
949 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000950 scm = 'svn'
951 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000952
953 def _GetChangeLists(self):
954 """Get all change lists."""
955 if self._changelists == None:
956 previous_cwd = os.getcwd()
957 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000958 # Need to import here to avoid circular dependency.
959 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000960 self._changelists = gcl.GetModifiedFiles()
961 os.chdir(previous_cwd)
962 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000963
964 def GetAllModifiedFiles(self):
965 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000966 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000967 all_modified_files = []
968 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000969 all_modified_files.extend(
970 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000971 return all_modified_files
972
973 def GetModifiedFiles(self):
974 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000975 changelists = self._GetChangeLists()
976 return [os.path.join(self.RepositoryRoot(), f[1])
977 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000978
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000979 def AllFiles(self, root=None):
980 """List all files under source control in the repo."""
981 root = root or self.RepositoryRoot()
982 return subprocess.check_output(
983 ['svn', 'ls', '-R', '.'], cwd=root).splitlines()
984
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000985
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000986class GitChange(Change):
987 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000988 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000989
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000990 def AllFiles(self, root=None):
991 """List all files under source control in the repo."""
992 root = root or self.RepositoryRoot()
993 return subprocess.check_output(
994 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
995
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000996
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000997def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998 """Finds all presubmit files that apply to a given set of source files.
999
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001000 If inherit-review-settings-ok is present right under root, looks for
1001 PRESUBMIT.py in directories enclosing root.
1002
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001003 Args:
1004 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001005 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006
1007 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001008 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001009 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001010 files = [normpath(os.path.join(root, f)) for f in files]
1011
1012 # List all the individual directories containing files.
1013 directories = set([os.path.dirname(f) for f in files])
1014
1015 # Ignore root if inherit-review-settings-ok is present.
1016 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1017 root = None
1018
1019 # Collect all unique directories that may contain PRESUBMIT.py.
1020 candidates = set()
1021 for directory in directories:
1022 while True:
1023 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001025 candidates.add(directory)
1026 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001027 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001028 parent_dir = os.path.dirname(directory)
1029 if parent_dir == directory:
1030 # We hit the system root directory.
1031 break
1032 directory = parent_dir
1033
1034 # Look for PRESUBMIT.py in all candidate directories.
1035 results = []
1036 for directory in sorted(list(candidates)):
1037 p = os.path.join(directory, 'PRESUBMIT.py')
1038 if os.path.isfile(p):
1039 results.append(p)
1040
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001041 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001042 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001043
1044
thestig@chromium.orgde243452009-10-06 21:02:56 +00001045class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001046 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001047 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +00001048 """Executes GetPreferredTrySlaves() from a single presubmit script.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001049
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001050 This will soon be deprecated and replaced by GetPreferredTryMasters().
thestig@chromium.orgde243452009-10-06 21:02:56 +00001051
1052 Args:
1053 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +00001054 presubmit_path: Project script to run.
1055 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001056
1057 Return:
1058 A list of try slaves.
1059 """
1060 context = {}
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001061 main_path = os.getcwd()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001062 try:
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001063 os.chdir(os.path.dirname(presubmit_path))
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001064 exec script_text in context
1065 except Exception, e:
1066 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001067 finally:
1068 os.chdir(main_path)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001069
1070 function_name = 'GetPreferredTrySlaves'
1071 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001072 get_preferred_try_slaves = context[function_name]
1073 function_info = inspect.getargspec(get_preferred_try_slaves)
1074 if len(function_info[0]) == 1:
1075 result = get_preferred_try_slaves(project)
1076 elif len(function_info[0]) == 2:
1077 result = get_preferred_try_slaves(project, change)
1078 else:
1079 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +00001080 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001081 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +00001082 'Presubmit functions must return a list, got a %s instead: %s' %
1083 (type(result), str(result)))
1084 for item in result:
stip@chromium.org68e04192013-11-04 22:14:38 +00001085 if isinstance(item, basestring):
1086 # Old-style ['bot'] format.
1087 botname = item
1088 elif isinstance(item, tuple):
1089 # New-style [('bot', set(['tests']))] format.
1090 botname = item[0]
1091 else:
1092 raise PresubmitFailure('PRESUBMIT.py returned invalid tryslave/test'
1093 ' format.')
1094
1095 if botname != botname.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001096 raise PresubmitFailure(
1097 'Try slave names cannot start/end with whitespace')
stip@chromium.org68e04192013-11-04 22:14:38 +00001098 if ',' in botname:
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +00001099 raise PresubmitFailure(
stip@chromium.org68e04192013-11-04 22:14:38 +00001100 'Do not use \',\' separated builder or test names: %s' % botname)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001101 else:
1102 result = []
stip@chromium.org5ca27622013-12-18 17:44:58 +00001103
1104 def valid_oldstyle(result):
1105 return all(isinstance(i, basestring) for i in result)
1106
1107 def valid_newstyle(result):
1108 return (all(isinstance(i, tuple) for i in result) and
1109 all(len(i) == 2 for i in result) and
1110 all(isinstance(i[0], basestring) for i in result) and
1111 all(isinstance(i[1], set) for i in result)
1112 )
1113
1114 # Ensure it's either all old-style or all new-style.
1115 if not valid_oldstyle(result) and not valid_newstyle(result):
1116 raise PresubmitFailure(
1117 'PRESUBMIT.py returned invalid trybot specification!')
1118
thestig@chromium.orgde243452009-10-06 21:02:56 +00001119 return result
1120
1121
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001122class GetTryMastersExecuter(object):
1123 @staticmethod
1124 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1125 """Executes GetPreferredTryMasters() from a single presubmit script.
1126
1127 Args:
1128 script_text: The text of the presubmit script.
1129 presubmit_path: Project script to run.
1130 project: Project name to pass to presubmit script for bot selection.
1131
1132 Return:
1133 A map of try masters to map of builders to set of tests.
1134 """
1135 context = {}
1136 try:
1137 exec script_text in context
1138 except Exception, e:
1139 raise PresubmitFailure('"%s" had an exception.\n%s'
1140 % (presubmit_path, e))
1141
1142 function_name = 'GetPreferredTryMasters'
1143 if function_name not in context:
1144 return {}
1145 get_preferred_try_masters = context[function_name]
1146 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1147 raise PresubmitFailure(
1148 'Expected function "GetPreferredTryMasters" to take two arguments.')
1149 return get_preferred_try_masters(project, change)
1150
1151
rmistry@google.com5626a922015-02-26 14:03:30 +00001152class GetPostUploadExecuter(object):
1153 @staticmethod
1154 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1155 """Executes PostUploadHook() from a single presubmit script.
1156
1157 Args:
1158 script_text: The text of the presubmit script.
1159 presubmit_path: Project script to run.
1160 cl: The Changelist object.
1161 change: The Change object.
1162
1163 Return:
1164 A list of results objects.
1165 """
1166 context = {}
1167 try:
1168 exec script_text in context
1169 except Exception, e:
1170 raise PresubmitFailure('"%s" had an exception.\n%s'
1171 % (presubmit_path, e))
1172
1173 function_name = 'PostUploadHook'
1174 if function_name not in context:
1175 return {}
1176 post_upload_hook = context[function_name]
1177 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1178 raise PresubmitFailure(
1179 'Expected function "PostUploadHook" to take three arguments.')
1180 return post_upload_hook(cl, change, OutputApi(False))
1181
1182
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001183def DoGetTrySlaves(change,
1184 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001185 repository_root,
1186 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +00001187 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001188 verbose,
1189 output_stream):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001190 """Get the list of try servers from the presubmit scripts (deprecated).
thestig@chromium.orgde243452009-10-06 21:02:56 +00001191
1192 Args:
1193 changed_files: List of modified files.
1194 repository_root: The repository root.
1195 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +00001196 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001197 verbose: Prints debug info.
1198 output_stream: A stream to write debug output to.
1199
1200 Return:
1201 List of try slaves
1202 """
1203 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1204 if not presubmit_files and verbose:
mdempsky@chromium.orgd59e7612014-03-05 19:55:56 +00001205 output_stream.write("Warning, no PRESUBMIT.py found.\n")
thestig@chromium.orgde243452009-10-06 21:02:56 +00001206 results = []
1207 executer = GetTrySlavesExecuter()
stip@chromium.org5ca27622013-12-18 17:44:58 +00001208
thestig@chromium.orgde243452009-10-06 21:02:56 +00001209 if default_presubmit:
1210 if verbose:
1211 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001212 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001213 results.extend(executer.ExecPresubmitScript(
1214 default_presubmit, fake_path, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001215 for filename in presubmit_files:
1216 filename = os.path.abspath(filename)
1217 if verbose:
1218 output_stream.write("Running %s\n" % filename)
1219 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001220 presubmit_script = gclient_utils.FileRead(filename, 'rU')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001221 results.extend(executer.ExecPresubmitScript(
1222 presubmit_script, filename, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001223
stip@chromium.org5ca27622013-12-18 17:44:58 +00001224
1225 slave_dict = {}
1226 old_style = filter(lambda x: isinstance(x, basestring), results)
1227 new_style = filter(lambda x: isinstance(x, tuple), results)
1228
1229 for result in new_style:
1230 slave_dict.setdefault(result[0], set()).update(result[1])
1231 slaves = list(slave_dict.items())
1232
1233 slaves.extend(set(old_style))
stip@chromium.org68e04192013-11-04 22:14:38 +00001234
thestig@chromium.orgde243452009-10-06 21:02:56 +00001235 if slaves and verbose:
stip@chromium.org5ca27622013-12-18 17:44:58 +00001236 output_stream.write(', '.join((str(x) for x in slaves)))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001237 output_stream.write('\n')
1238 return slaves
1239
1240
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001241def _MergeMasters(masters1, masters2):
1242 """Merges two master maps. Merges also the tests of each builder."""
1243 result = {}
1244 for (master, builders) in itertools.chain(masters1.iteritems(),
1245 masters2.iteritems()):
1246 new_builders = result.setdefault(master, {})
1247 for (builder, tests) in builders.iteritems():
1248 new_builders.setdefault(builder, set([])).update(tests)
1249 return result
1250
1251
1252def DoGetTryMasters(change,
1253 changed_files,
1254 repository_root,
1255 default_presubmit,
1256 project,
1257 verbose,
1258 output_stream):
1259 """Get the list of try masters from the presubmit scripts.
1260
1261 Args:
1262 changed_files: List of modified files.
1263 repository_root: The repository root.
1264 default_presubmit: A default presubmit script to execute in any case.
1265 project: Optional name of a project used in selecting trybots.
1266 verbose: Prints debug info.
1267 output_stream: A stream to write debug output to.
1268
1269 Return:
1270 Map of try masters to map of builders to set of tests.
1271 """
1272 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1273 if not presubmit_files and verbose:
1274 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1275 results = {}
1276 executer = GetTryMastersExecuter()
1277
1278 if default_presubmit:
1279 if verbose:
1280 output_stream.write("Running default presubmit script.\n")
1281 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1282 results = _MergeMasters(results, executer.ExecPresubmitScript(
1283 default_presubmit, fake_path, project, change))
1284 for filename in presubmit_files:
1285 filename = os.path.abspath(filename)
1286 if verbose:
1287 output_stream.write("Running %s\n" % filename)
1288 # Accept CRLF presubmit script.
1289 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1290 results = _MergeMasters(results, executer.ExecPresubmitScript(
1291 presubmit_script, filename, project, change))
1292
1293 # Make sets to lists again for later JSON serialization.
1294 for builders in results.itervalues():
1295 for builder in builders:
1296 builders[builder] = list(builders[builder])
1297
1298 if results and verbose:
1299 output_stream.write('%s\n' % str(results))
1300 return results
1301
1302
rmistry@google.com5626a922015-02-26 14:03:30 +00001303def DoPostUploadExecuter(change,
1304 cl,
1305 repository_root,
1306 verbose,
1307 output_stream):
1308 """Execute the post upload hook.
1309
1310 Args:
1311 change: The Change object.
1312 cl: The Changelist object.
1313 repository_root: The repository root.
1314 verbose: Prints debug info.
1315 output_stream: A stream to write debug output to.
1316 """
1317 presubmit_files = ListRelevantPresubmitFiles(
1318 change.LocalPaths(), repository_root)
1319 if not presubmit_files and verbose:
1320 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1321 results = []
1322 executer = GetPostUploadExecuter()
1323 # The root presubmit file should be executed after the ones in subdirectories.
1324 # i.e. the specific post upload hooks should run before the general ones.
1325 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1326 presubmit_files.reverse()
1327
1328 for filename in presubmit_files:
1329 filename = os.path.abspath(filename)
1330 if verbose:
1331 output_stream.write("Running %s\n" % filename)
1332 # Accept CRLF presubmit script.
1333 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1334 results.extend(executer.ExecPresubmitScript(
1335 presubmit_script, filename, cl, change))
1336 output_stream.write('\n')
1337 if results:
1338 output_stream.write('** Post Upload Hook Messages **\n')
1339 for result in results:
1340 result.handle(output_stream)
1341 output_stream.write('\n')
1342
1343 return results
1344
1345
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001346class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001347 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001348 """
1349 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001350 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001351 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001352 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001353 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001354 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001355 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001356 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001357 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001358
1359 def ExecPresubmitScript(self, script_text, presubmit_path):
1360 """Executes a single presubmit script.
1361
1362 Args:
1363 script_text: The text of the presubmit script.
1364 presubmit_path: The path to the presubmit file (this will be reported via
1365 input_api.PresubmitLocalPath()).
1366
1367 Return:
1368 A list of result objects, empty if no problems.
1369 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001370
chase@chromium.org8e416c82009-10-06 04:30:44 +00001371 # Change to the presubmit file's directory to support local imports.
1372 main_path = os.getcwd()
1373 os.chdir(os.path.dirname(presubmit_path))
1374
1375 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001376 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001377 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001378 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001379 try:
1380 exec script_text in context
1381 except Exception, e:
1382 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001383
1384 # These function names must change if we make substantial changes to
1385 # the presubmit API that are not backwards compatible.
1386 if self.committing:
1387 function_name = 'CheckChangeOnCommit'
1388 else:
1389 function_name = 'CheckChangeOnUpload'
1390 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001391 context['__args'] = (input_api, OutputApi(self.committing))
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001392 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001393 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001394 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001395 if not (isinstance(result, types.TupleType) or
1396 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001397 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001398 'Presubmit functions must return a tuple or list')
1399 for item in result:
1400 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001401 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001402 'All presubmit results must be of types derived from '
1403 'output_api.PresubmitResult')
1404 else:
1405 result = () # no error since the script doesn't care about current event.
1406
chase@chromium.org8e416c82009-10-06 04:30:44 +00001407 # Return the process to the original working directory.
1408 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001409 return result
1410
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001411
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001412def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001413 committing,
1414 verbose,
1415 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001416 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001417 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001418 may_prompt,
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001419 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001420 """Runs all presubmit checks that apply to the files in the change.
1421
1422 This finds all PRESUBMIT.py files in directories enclosing the files in the
1423 change (up to the repository root) and calls the relevant entrypoint function
1424 depending on whether the change is being committed or uploaded.
1425
1426 Prints errors, warnings and notifications. Prompts the user for warnings
1427 when needed.
1428
1429 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001430 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001431 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1432 verbose: Prints debug info.
1433 output_stream: A stream to write output from presubmit tests to.
1434 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001435 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001436 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001437 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001438
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001439 Warning:
1440 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1441 SHOULD be sys.stdin.
1442
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001443 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001444 A PresubmitOutput object. Use output.should_continue() to figure out
1445 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001446 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001447 old_environ = os.environ
1448 try:
1449 # Make sure python subprocesses won't generate .pyc files.
1450 os.environ = os.environ.copy()
1451 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001452
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001453 output = PresubmitOutput(input_stream, output_stream)
1454 if committing:
1455 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001456 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001457 output.write("Running presubmit upload checks ...\n")
1458 start_time = time.time()
1459 presubmit_files = ListRelevantPresubmitFiles(
1460 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1461 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001462 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001463 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001464 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001465 if default_presubmit:
1466 if verbose:
1467 output.write("Running default presubmit script.\n")
1468 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1469 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1470 for filename in presubmit_files:
1471 filename = os.path.abspath(filename)
1472 if verbose:
1473 output.write("Running %s\n" % filename)
1474 # Accept CRLF presubmit script.
1475 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1476 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001477
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001478 errors = []
1479 notifications = []
1480 warnings = []
1481 for result in results:
1482 if result.fatal:
1483 errors.append(result)
1484 elif result.should_prompt:
1485 warnings.append(result)
1486 else:
1487 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001488
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001489 output.write('\n')
1490 for name, items in (('Messages', notifications),
1491 ('Warnings', warnings),
1492 ('ERRORS', errors)):
1493 if items:
1494 output.write('** Presubmit %s **\n' % name)
1495 for item in items:
1496 item.handle(output)
1497 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001498
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001499 total_time = time.time() - start_time
1500 if total_time > 1.0:
1501 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001502
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001503 if not errors:
1504 if not warnings:
1505 output.write('Presubmit checks passed.\n')
1506 elif may_prompt:
1507 output.prompt_yes_no('There were presubmit warnings. '
1508 'Are you sure you wish to continue? (y/N): ')
1509 else:
1510 output.fail()
1511
1512 global _ASKED_FOR_FEEDBACK
1513 # Ask for feedback one time out of 5.
1514 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001515 output.write(
1516 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1517 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1518 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001519 _ASKED_FOR_FEEDBACK = True
1520 return output
1521 finally:
1522 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001523
1524
1525def ScanSubDirs(mask, recursive):
1526 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001527 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001528 else:
1529 results = []
1530 for root, dirs, files in os.walk('.'):
1531 if '.svn' in dirs:
1532 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001533 if '.git' in dirs:
1534 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001535 for name in files:
1536 if fnmatch.fnmatch(name, mask):
1537 results.append(os.path.join(root, name))
1538 return results
1539
1540
1541def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001542 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001543 files = []
1544 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001545 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001546 return files
1547
1548
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001549def load_files(options, args):
1550 """Tries to determine the SCM."""
1551 change_scm = scm.determine_scm(options.root)
1552 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001553 if args:
1554 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001555 if change_scm == 'svn':
1556 change_class = SvnChange
1557 if not files:
1558 files = scm.SVN.CaptureStatus([], options.root)
1559 elif change_scm == 'git':
1560 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001561 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001562 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001563 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001564 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001565 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1566 if not files:
1567 return None, None
1568 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001569 return change_class, files
1570
1571
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001572class NonexistantCannedCheckFilter(Exception):
1573 pass
1574
1575
1576@contextlib.contextmanager
1577def canned_check_filter(method_names):
1578 filtered = {}
1579 try:
1580 for method_name in method_names:
1581 if not hasattr(presubmit_canned_checks, method_name):
1582 raise NonexistantCannedCheckFilter(method_name)
1583 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1584 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1585 yield
1586 finally:
1587 for name, method in filtered.iteritems():
1588 setattr(presubmit_canned_checks, name, method)
1589
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001590
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001591def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001592 """Runs an external program, potentially from a child process created by the
1593 multiprocessing module.
1594
1595 multiprocessing needs a top level function with a single argument.
1596 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001597 cmd_data.kwargs['stdout'] = subprocess.PIPE
1598 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1599 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001600 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001601 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001602 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001603 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001604 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001605 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001606 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1607 if code != 0:
1608 return cmd_data.message(
1609 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1610 if cmd_data.info:
1611 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001612
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001613
sbc@chromium.org013731e2015-02-26 18:28:43 +00001614def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001615 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001616 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001617 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001618 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001619 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1620 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001621 parser.add_option("-r", "--recursive", action="store_true",
1622 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001623 parser.add_option("-v", "--verbose", action="count", default=0,
1624 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001625 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001626 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001627 parser.add_option("--description", default='')
1628 parser.add_option("--issue", type='int', default=0)
1629 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001630 parser.add_option("--root", default=os.getcwd(),
1631 help="Search for PRESUBMIT.py up to this directory. "
1632 "If inherit-review-settings-ok is present in this "
1633 "directory, parent directories up to the root file "
1634 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001635 parser.add_option("--upstream",
1636 help="Git only: the base ref or upstream branch against "
1637 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001638 parser.add_option("--default_presubmit")
1639 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001640 parser.add_option("--skip_canned", action='append', default=[],
1641 help="A list of checks to skip which appear in "
1642 "presubmit_canned_checks. Can be provided multiple times "
1643 "to skip multiple canned checks.")
maruel@chromium.org239f4112011-06-03 20:08:23 +00001644 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1645 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001646 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1647 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001648 # These are for OAuth2 authentication for bots. See also apply_issue.py
1649 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1650 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1651
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001652 parser.add_option("--trybot-json",
1653 help="Output trybot information to the file specified.")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001654 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001655 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001656 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001657
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001658 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001659 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001660 elif options.verbose:
1661 logging.basicConfig(level=logging.INFO)
1662 else:
1663 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001664
1665 if options.rietveld_email and options.rietveld_email_file:
1666 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1667 "can be passed to this program.")
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001668
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001669 if options.rietveld_email_file:
1670 with open(options.rietveld_email_file, "rb") as f:
1671 options.rietveld_email = f.read().strip()
1672
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001673 change_class, files = load_files(options, args)
1674 if not change_class:
1675 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001676 logging.info('Found %d file(s).' % len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001677
maruel@chromium.org239f4112011-06-03 20:08:23 +00001678 rietveld_obj = None
1679 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001680 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001681 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001682 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1683 options.rietveld_url,
1684 options.rietveld_email,
1685 options.rietveld_private_key_file)
1686 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001687 rietveld_obj = rietveld.CachingRietveld(
1688 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001689 auth_config,
1690 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001691 if options.rietveld_fetch:
1692 assert options.issue
1693 props = rietveld_obj.get_issue_properties(options.issue, False)
1694 options.author = props['owner_email']
1695 options.description = props['description']
1696 logging.info('Got author: "%s"', options.author)
1697 logging.info('Got description: """\n%s\n"""', options.description)
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001698 if options.trybot_json:
1699 with open(options.trybot_json, 'w') as f:
1700 # Python's sets aren't JSON-encodable, so we convert them to lists here.
1701 class SetEncoder(json.JSONEncoder):
1702 # pylint: disable=E0202
1703 def default(self, obj):
1704 if isinstance(obj, set):
1705 return sorted(obj)
1706 return json.JSONEncoder.default(self, obj)
1707 change = change_class(options.name,
1708 options.description,
1709 options.root,
1710 files,
1711 options.issue,
1712 options.patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001713 options.author,
1714 upstream=options.upstream)
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001715 trybots = DoGetTrySlaves(
1716 change,
1717 change.LocalPaths(),
1718 change.RepositoryRoot(),
1719 None,
1720 None,
1721 options.verbose,
1722 sys.stdout)
1723 json.dump(trybots, f, cls=SetEncoder)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001724 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001725 with canned_check_filter(options.skip_canned):
1726 results = DoPresubmitChecks(
1727 change_class(options.name,
1728 options.description,
1729 options.root,
1730 files,
1731 options.issue,
1732 options.patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001733 options.author,
1734 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001735 options.commit,
1736 options.verbose,
1737 sys.stdout,
1738 sys.stdin,
1739 options.default_presubmit,
1740 options.may_prompt,
1741 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001742 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001743 except NonexistantCannedCheckFilter, e:
1744 print >> sys.stderr, (
1745 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1746 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001747 except PresubmitFailure, e:
1748 print >> sys.stderr, e
1749 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1750 print >> sys.stderr, 'If all fails, contact maruel@'
1751 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001752
1753
1754if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001755 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001756 try:
1757 sys.exit(main())
1758 except KeyboardInterrupt:
1759 sys.stderr.write('interrupted\n')
1760 sys.exit(1)