blob: b386524068c1ceddc44fc4e10edc4c596d6f577c [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.
maruel@chromium.org35625c72011-03-23 17:34:02 +000042import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000043import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000044import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000045import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000046import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000047import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000048import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000049
50
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000051# Ask for feedback only once in program lifetime.
52_ASKED_FOR_FEEDBACK = False
53
54
maruel@chromium.org899e1c12011-04-07 17:03:18 +000055class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000056 pass
57
58
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000059class CommandData(object):
60 def __init__(self, name, cmd, kwargs, message):
61 self.name = name
62 self.cmd = cmd
63 self.kwargs = kwargs
64 self.message = message
65 self.info = None
66
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000067
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000068def normpath(path):
69 '''Version of os.path.normpath that also changes backward slashes to
70 forward slashes when not running on Windows.
71 '''
72 # This is safe to always do because the Windows version of os.path.normpath
73 # will replace forward slashes with backward slashes.
74 path = path.replace(os.sep, '/')
75 return os.path.normpath(path)
76
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000077
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000078def _RightHandSideLinesImpl(affected_files):
79 """Implements RightHandSideLines for InputApi and GclChange."""
80 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000081 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000082 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000083 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000084
85
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000086class PresubmitOutput(object):
87 def __init__(self, input_stream=None, output_stream=None):
88 self.input_stream = input_stream
89 self.output_stream = output_stream
90 self.reviewers = []
91 self.written_output = []
92 self.error_count = 0
93
94 def prompt_yes_no(self, prompt_string):
95 self.write(prompt_string)
96 if self.input_stream:
97 response = self.input_stream.readline().strip().lower()
98 if response not in ('y', 'yes'):
99 self.fail()
100 else:
101 self.fail()
102
103 def fail(self):
104 self.error_count += 1
105
106 def should_continue(self):
107 return not self.error_count
108
109 def write(self, s):
110 self.written_output.append(s)
111 if self.output_stream:
112 self.output_stream.write(s)
113
114 def getvalue(self):
115 return ''.join(self.written_output)
116
117
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000118# Top level object so multiprocessing can pickle
119# Public access through OutputApi object.
120class _PresubmitResult(object):
121 """Base class for result objects."""
122 fatal = False
123 should_prompt = False
124
125 def __init__(self, message, items=None, long_text=''):
126 """
127 message: A short one-line message to indicate errors.
128 items: A list of short strings to indicate where errors occurred.
129 long_text: multi-line text output, e.g. from another tool
130 """
131 self._message = message
132 self._items = items or []
133 if items:
134 self._items = items
135 self._long_text = long_text.rstrip()
136
137 def handle(self, output):
138 output.write(self._message)
139 output.write('\n')
140 for index, item in enumerate(self._items):
141 output.write(' ')
142 # Write separately in case it's unicode.
143 output.write(str(item))
144 if index < len(self._items) - 1:
145 output.write(' \\')
146 output.write('\n')
147 if self._long_text:
148 output.write('\n***************\n')
149 # Write separately in case it's unicode.
150 output.write(self._long_text)
151 output.write('\n***************\n')
152 if self.fatal:
153 output.fail()
154
155
156# Top level object so multiprocessing can pickle
157# Public access through OutputApi object.
158class _PresubmitAddReviewers(_PresubmitResult):
159 """Add some suggested reviewers to the change."""
160 def __init__(self, reviewers):
161 super(_PresubmitAddReviewers, self).__init__('')
162 self.reviewers = reviewers
163
164 def handle(self, output):
165 output.reviewers.extend(self.reviewers)
166
167
168# Top level object so multiprocessing can pickle
169# Public access through OutputApi object.
170class _PresubmitError(_PresubmitResult):
171 """A hard presubmit error."""
172 fatal = True
173
174
175# Top level object so multiprocessing can pickle
176# Public access through OutputApi object.
177class _PresubmitPromptWarning(_PresubmitResult):
178 """An warning that prompts the user if they want to continue."""
179 should_prompt = True
180
181
182# Top level object so multiprocessing can pickle
183# Public access through OutputApi object.
184class _PresubmitNotifyResult(_PresubmitResult):
185 """Just print something to the screen -- but it's not even a warning."""
186 pass
187
188
189# Top level object so multiprocessing can pickle
190# Public access through OutputApi object.
191class _MailTextResult(_PresubmitResult):
192 """A warning that should be included in the review request email."""
193 def __init__(self, *args, **kwargs):
194 super(_MailTextResult, self).__init__()
195 raise NotImplementedError()
196
197
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000198class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000199 """An instance of OutputApi gets passed to presubmit scripts so that they
200 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000201 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000202 PresubmitResult = _PresubmitResult
203 PresubmitAddReviewers = _PresubmitAddReviewers
204 PresubmitError = _PresubmitError
205 PresubmitPromptWarning = _PresubmitPromptWarning
206 PresubmitNotifyResult = _PresubmitNotifyResult
207 MailTextResult = _MailTextResult
208
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000209 def __init__(self, is_committing):
210 self.is_committing = is_committing
211
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000212 def PresubmitPromptOrNotify(self, *args, **kwargs):
213 """Warn the user when uploading, but only notify if committing."""
214 if self.is_committing:
215 return self.PresubmitNotifyResult(*args, **kwargs)
216 return self.PresubmitPromptWarning(*args, **kwargs)
217
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000218
219class InputApi(object):
220 """An instance of this object is passed to presubmit scripts so they can
221 know stuff about the change they're looking at.
222 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000223 # Method could be a function
224 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000225
maruel@chromium.org3410d912009-06-09 20:56:16 +0000226 # File extensions that are considered source files from a style guide
227 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000228 #
229 # Files without an extension aren't included in the list. If you want to
230 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
231 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000232 DEFAULT_WHITE_LIST = (
233 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000234 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
235 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000236 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000237 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000238 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000239 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000240 )
241
242 # Path regexp that should be excluded from being considered containing source
243 # files. Don't modify this list from a presubmit script!
244 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000245 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000246 r".*\bexperimental[\\\/].*",
247 r".*\bthird_party[\\\/].*",
248 # Output directories (just in case)
249 r".*\bDebug[\\\/].*",
250 r".*\bRelease[\\\/].*",
251 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000252 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000253 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000254 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000255 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000256 r"(|.*[\\\/])\.git[\\\/].*",
257 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000258 # There is no point in processing a patch file.
259 r".+\.diff$",
260 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000261 )
262
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000263 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000264 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000265 """Builds an InputApi object.
266
267 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000268 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000269 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000270 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000271 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000272 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000273 # Version number of the presubmit_support script.
274 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000275 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000276 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000277 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000278 # TBD
279 self.host_url = 'http://codereview.chromium.org'
280 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000281 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000282
283 # We expose various modules and functions as attributes of the input_api
284 # so that presubmit scripts don't have to import them.
285 self.basename = os.path.basename
286 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000287 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000288 self.cStringIO = cStringIO
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000289 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000290 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000291 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000292 self.os_listdir = os.listdir
293 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000294 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000295 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000296 self.pickle = pickle
297 self.marshal = marshal
298 self.re = re
299 self.subprocess = subprocess
300 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000301 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000302 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000303 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000304 self.urllib2 = urllib2
305
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000306 # To easily fork python.
307 self.python_executable = sys.executable
308 self.environ = os.environ
309
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000310 # InputApi.platform is the platform you're currently running on.
311 self.platform = sys.platform
312
313 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000314 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000315
316 # We carry the canned checks so presubmit scripts can easily use them.
317 self.canned_checks = presubmit_canned_checks
318
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000319 # TODO(dpranke): figure out a list of all approved owners for a repo
320 # in order to be able to handle wildcard OWNERS files?
321 self.owners_db = owners.Database(change.RepositoryRoot(),
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000322 fopen=file, os_path=self.os_path, glob=self.glob)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000323 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000324 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000325
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000326 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000327 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000328 # Access to a protected member _XX of a client class
329 # pylint: disable=W0212
330 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000331 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000332 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
333 for (a, b, header) in cpplint._re_pattern_templates
334 ]
335
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000336 def PresubmitLocalPath(self):
337 """Returns the local path of the presubmit script currently being run.
338
339 This is useful if you don't want to hard-code absolute paths in the
340 presubmit script. For example, It can be used to find another file
341 relative to the PRESUBMIT.py script, so the whole tree can be branched and
342 the presubmit script still works, without editing its content.
343 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000344 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000345
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000346 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000347 """Translate a depot path to a local path (relative to client root).
348
349 Args:
350 Depot path as a string.
351
352 Returns:
353 The local path of the depot path under the user's current client, or None
354 if the file is not mapped.
355
356 Remember to check for the None case and show an appropriate error!
357 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000358 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
359 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000360
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000361 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000362 """Translate a local path to a depot path.
363
364 Args:
365 Local path (relative to current directory, or absolute) as a string.
366
367 Returns:
368 The depot path (SVN URL) of the file if mapped, otherwise None.
369 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000370 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
371 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000372
sail@chromium.org5538e022011-05-12 17:53:16 +0000373 def AffectedFiles(self, include_dirs=False, include_deletes=True,
374 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000375 """Same as input_api.change.AffectedFiles() except only lists files
376 (and optionally directories) in the same directory as the current presubmit
377 script, or subdirectories thereof.
378 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000379 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000380 if len(dir_with_slash) == 1:
381 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000382
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000383 return filter(
384 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000385 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000386
387 def LocalPaths(self, include_dirs=False):
388 """Returns local paths of input_api.AffectedFiles()."""
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000389 paths = [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
390 logging.debug("LocalPaths: %s", paths)
391 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000392
393 def AbsoluteLocalPaths(self, include_dirs=False):
394 """Returns absolute local paths of input_api.AffectedFiles()."""
395 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
396
397 def ServerPaths(self, include_dirs=False):
398 """Returns server paths of input_api.AffectedFiles()."""
399 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
400
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000401 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000402 """Same as input_api.change.AffectedTextFiles() except only lists files
403 in the same directory as the current presubmit script, or subdirectories
404 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000405 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000406 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000407 warn("AffectedTextFiles(include_deletes=%s)"
408 " is deprecated and ignored" % str(include_deletes),
409 category=DeprecationWarning,
410 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000411 return filter(lambda x: x.IsTextFile(),
412 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000413
maruel@chromium.org3410d912009-06-09 20:56:16 +0000414 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
415 """Filters out files that aren't considered "source file".
416
417 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
418 and InputApi.DEFAULT_BLACK_LIST is used respectively.
419
420 The lists will be compiled as regular expression and
421 AffectedFile.LocalPath() needs to pass both list.
422
423 Note: Copy-paste this function to suit your needs or use a lambda function.
424 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000425 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000426 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000427 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000428 if self.re.match(item, local_path):
429 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000430 return True
431 return False
432 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
433 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
434
435 def AffectedSourceFiles(self, source_file):
436 """Filter the list of AffectedTextFiles by the function source_file.
437
438 If source_file is None, InputApi.FilterSourceFile() is used.
439 """
440 if not source_file:
441 source_file = self.FilterSourceFile
442 return filter(source_file, self.AffectedTextFiles())
443
444 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000445 """An iterator over all text lines in "new" version of changed files.
446
447 Only lists lines from new or modified text files in the change that are
448 contained by the directory of the currently executing presubmit script.
449
450 This is useful for doing line-by-line regex checks, like checking for
451 trailing whitespace.
452
453 Yields:
454 a 3 tuple:
455 the AffectedFile instance of the current file;
456 integer line number (1-based); and
457 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000458
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000459 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000460 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000461 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000462 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000463
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000464 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000465 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000466
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000467 Deny reading anything outside the repository.
468 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000469 if isinstance(file_item, AffectedFile):
470 file_item = file_item.AbsoluteLocalPath()
471 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000472 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000473 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000474
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000475 @property
476 def tbr(self):
477 """Returns if a change is TBR'ed."""
478 return 'TBR' in self.change.tags
479
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000480 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000481 tests = []
482 msgs = []
483 for t in tests_mix:
484 if isinstance(t, OutputApi.PresubmitResult):
485 msgs.append(t)
486 else:
487 assert issubclass(t.message, _PresubmitResult)
488 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000489 if self.verbose:
490 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000491 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000492 pool = multiprocessing.Pool()
493 # async recipe works around multiprocessing bug handling Ctrl-C
494 msgs.extend(pool.map_async(CallCommand, tests).get(99999))
495 pool.close()
496 pool.join()
497 else:
498 msgs.extend(map(CallCommand, tests))
499 return [m for m in msgs if m]
500
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000501
nick@chromium.orgff526192013-06-10 19:30:26 +0000502class _DiffCache(object):
503 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000504 def __init__(self, upstream=None):
505 """Stores the upstream revision against which all diffs will be computed."""
506 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000507
508 def GetDiff(self, path, local_root):
509 """Get the diff for a particular path."""
510 raise NotImplementedError()
511
512
513class _SvnDiffCache(_DiffCache):
514 """DiffCache implementation for subversion."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000515 def __init__(self, *args, **kwargs):
516 super(_SvnDiffCache, self).__init__(*args, **kwargs)
nick@chromium.orgff526192013-06-10 19:30:26 +0000517 self._diffs_by_file = {}
518
519 def GetDiff(self, path, local_root):
520 if path not in self._diffs_by_file:
521 self._diffs_by_file[path] = scm.SVN.GenerateDiff([path], local_root,
522 False, None)
523 return self._diffs_by_file[path]
524
525
526class _GitDiffCache(_DiffCache):
527 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000528 def __init__(self, upstream):
529 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000530 self._diffs_by_file = None
531
532 def GetDiff(self, path, local_root):
533 if not self._diffs_by_file:
534 # Compute a single diff for all files and parse the output; should
535 # with git this is much faster than computing one diff for each file.
536 diffs = {}
537
538 # Don't specify any filenames below, because there are command line length
539 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000540 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
541 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000542
543 # This regex matches the path twice, separated by a space. Note that
544 # filename itself may contain spaces.
545 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
546 current_diff = []
547 keep_line_endings = True
548 for x in unified_diff.splitlines(keep_line_endings):
549 match = file_marker.match(x)
550 if match:
551 # Marks the start of a new per-file section.
552 diffs[match.group('filename')] = current_diff = [x]
553 elif x.startswith('diff --git'):
554 raise PresubmitFailure('Unexpected diff line: %s' % x)
555 else:
556 current_diff.append(x)
557
558 self._diffs_by_file = dict(
559 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
560
561 if path not in self._diffs_by_file:
562 raise PresubmitFailure(
563 'Unified diff did not contain entry for file %s' % path)
564
565 return self._diffs_by_file[path]
566
567
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000568class AffectedFile(object):
569 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000570
571 DIFF_CACHE = _DiffCache
572
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000573 # Method could be a function
574 # pylint: disable=R0201
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000575 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000576 self._path = path
577 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000578 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000579 self._is_directory = None
580 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000581 self._cached_changed_contents = None
582 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000583 self._diff_cache = diff_cache
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000584 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000585
586 def ServerPath(self):
587 """Returns a path string that identifies the file in the SCM system.
588
589 Returns the empty string if the file does not exist in SCM.
590 """
nick@chromium.orgff526192013-06-10 19:30:26 +0000591 return ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000592
593 def LocalPath(self):
594 """Returns the path of this file on the local disk relative to client root.
595 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000596 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000597
598 def AbsoluteLocalPath(self):
599 """Returns the absolute path of this file on the local disk.
600 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000601 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000602
603 def IsDirectory(self):
604 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000605 if self._is_directory is None:
606 path = self.AbsoluteLocalPath()
607 self._is_directory = (os.path.exists(path) and
608 os.path.isdir(path))
609 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000610
611 def Action(self):
612 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000613 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
614 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000615 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000616
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000617 def Property(self, property_name):
618 """Returns the specified SCM property of this file, or None if no such
619 property.
620 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000621 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000622
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000623 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000624 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000625
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000626 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000627 raise NotImplementedError() # Implement when needed
628
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000629 def NewContents(self):
630 """Returns an iterator over the lines in the new version of file.
631
632 The new version is the file in the user's workspace, i.e. the "right hand
633 side".
634
635 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000636 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000637 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000638 if self._cached_new_contents is None:
639 self._cached_new_contents = []
640 if not self.IsDirectory():
641 try:
642 self._cached_new_contents = gclient_utils.FileRead(
643 self.AbsoluteLocalPath(), 'rU').splitlines()
644 except IOError:
645 pass # File not found? That's fine; maybe it was deleted.
646 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000647
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000648 def ChangedContents(self):
649 """Returns a list of tuples (line number, line text) of all new lines.
650
651 This relies on the scm diff output describing each changed code section
652 with a line of the form
653
654 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
655 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000656 if self._cached_changed_contents is not None:
657 return self._cached_changed_contents[:]
658 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000659 line_num = 0
660
661 if self.IsDirectory():
662 return []
663
664 for line in self.GenerateScmDiff().splitlines():
665 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
666 if m:
667 line_num = int(m.groups(1)[0])
668 continue
669 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000670 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000671 if not line.startswith('-'):
672 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000673 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000674
maruel@chromium.org5de13972009-06-10 18:16:06 +0000675 def __str__(self):
676 return self.LocalPath()
677
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000678 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000679 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680
maruel@chromium.org58407af2011-04-12 23:15:57 +0000681
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000682class SvnAffectedFile(AffectedFile):
683 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000684 # Method 'NNN' is abstract in class 'NNN' but is not overridden
685 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000686
nick@chromium.orgff526192013-06-10 19:30:26 +0000687 DIFF_CACHE = _SvnDiffCache
688
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000689 def __init__(self, *args, **kwargs):
690 AffectedFile.__init__(self, *args, **kwargs)
691 self._server_path = None
692 self._is_text_file = None
693
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000694 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000695 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000696 self._server_path = scm.SVN.CaptureLocalInfo(
697 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000698 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000699
700 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000701 if self._is_directory is None:
702 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000703 if os.path.exists(path):
704 # Retrieve directly from the file system; it is much faster than
705 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000706 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000707 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000708 self._is_directory = scm.SVN.CaptureLocalInfo(
709 [self.LocalPath()], self._local_root
710 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000711 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000712
713 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000714 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000715 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000716 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000717 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000718
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000719 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000720 if self._is_text_file is None:
721 if self.Action() == 'D':
722 # A deleted file is not a text file.
723 self._is_text_file = False
724 elif self.IsDirectory():
725 self._is_text_file = False
726 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000727 mime_type = scm.SVN.GetFileProperty(
728 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000729 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
730 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000731
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000732
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000733class GitAffectedFile(AffectedFile):
734 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000735 # Method 'NNN' is abstract in class 'NNN' but is not overridden
736 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000737
nick@chromium.orgff526192013-06-10 19:30:26 +0000738 DIFF_CACHE = _GitDiffCache
739
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000740 def __init__(self, *args, **kwargs):
741 AffectedFile.__init__(self, *args, **kwargs)
742 self._server_path = None
743 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000744
745 def ServerPath(self):
746 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000747 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000748 return self._server_path
749
750 def IsDirectory(self):
751 if self._is_directory is None:
752 path = self.AbsoluteLocalPath()
753 if os.path.exists(path):
754 # Retrieve directly from the file system; it is much faster than
755 # querying subversion, especially on Windows.
756 self._is_directory = os.path.isdir(path)
757 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000758 self._is_directory = False
759 return self._is_directory
760
761 def Property(self, property_name):
762 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000763 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000764 return self._properties[property_name]
765
766 def IsTextFile(self):
767 if self._is_text_file is None:
768 if self.Action() == 'D':
769 # A deleted file is not a text file.
770 self._is_text_file = False
771 elif self.IsDirectory():
772 self._is_text_file = False
773 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000774 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
775 return self._is_text_file
776
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000777
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000778class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000779 """Describe a change.
780
781 Used directly by the presubmit scripts to query the current change being
782 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000783
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000784 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000785 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000786 self.KEY: equivalent to tags['KEY']
787 """
788
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000789 _AFFECTED_FILES = AffectedFile
790
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000791 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000792 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000793 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000794 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000795
maruel@chromium.org58407af2011-04-12 23:15:57 +0000796 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000797 self, name, description, local_root, files, issue, patchset, author,
798 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000799 if files is None:
800 files = []
801 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000802 # Convert root into an absolute path.
803 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000804 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000805 self.issue = issue
806 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000807 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000808
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000809 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000810 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000811 self._description_without_tags = ''
812 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000813
maruel@chromium.orge085d812011-10-10 19:49:15 +0000814 assert all(
815 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
816
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000817 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000818 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000819 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
820 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000821 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000822
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000823 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000824 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000825 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000826
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000827 def DescriptionText(self):
828 """Returns the user-entered changelist description, minus tags.
829
830 Any line in the user-provided description starting with e.g. "FOO="
831 (whitespace permitted before and around) is considered a tag line. Such
832 lines are stripped out of the description this function returns.
833 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000834 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000835
836 def FullDescriptionText(self):
837 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000838 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000839
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000840 def SetDescriptionText(self, description):
841 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000842
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000843 Also updates the list of tags."""
844 self._full_description = description
845
846 # From the description text, build up a dictionary of key/value pairs
847 # plus the description minus all key/value or "tag" lines.
848 description_without_tags = []
849 self.tags = {}
850 for line in self._full_description.splitlines():
851 m = self.TAG_LINE_RE.match(line)
852 if m:
853 self.tags[m.group('key')] = m.group('value')
854 else:
855 description_without_tags.append(line)
856
857 # Change back to text and remove whitespace at end.
858 self._description_without_tags = (
859 '\n'.join(description_without_tags).rstrip())
860
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000861 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000862 """Returns the repository (checkout) root directory for this change,
863 as an absolute path.
864 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000865 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000866
867 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000868 """Return tags directly as attributes on the object."""
869 if not re.match(r"^[A-Z_]*$", attr):
870 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000871 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000872
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000873 def AllFiles(self, root=None):
874 """List all files under source control in the repo."""
875 raise NotImplementedError()
876
sail@chromium.org5538e022011-05-12 17:53:16 +0000877 def AffectedFiles(self, include_dirs=False, include_deletes=True,
878 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000879 """Returns a list of AffectedFile instances for all files in the change.
880
881 Args:
882 include_deletes: If false, deleted files will be filtered out.
883 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000884 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000885
886 Returns:
887 [AffectedFile(path, action), AffectedFile(path, action)]
888 """
889 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000890 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000891 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000892 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000893
sail@chromium.org5538e022011-05-12 17:53:16 +0000894 affected = filter(file_filter, affected)
895
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000896 if include_deletes:
897 return affected
898 else:
899 return filter(lambda x: x.Action() != 'D', affected)
900
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000901 def AffectedTextFiles(self, include_deletes=None):
902 """Return a list of the existing text files in a change."""
903 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000904 warn("AffectedTextFiles(include_deletes=%s)"
905 " is deprecated and ignored" % str(include_deletes),
906 category=DeprecationWarning,
907 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000908 return filter(lambda x: x.IsTextFile(),
909 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000910
911 def LocalPaths(self, include_dirs=False):
912 """Convenience function."""
913 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
914
915 def AbsoluteLocalPaths(self, include_dirs=False):
916 """Convenience function."""
917 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
918
919 def ServerPaths(self, include_dirs=False):
920 """Convenience function."""
921 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
922
923 def RightHandSideLines(self):
924 """An iterator over all text lines in "new" version of changed files.
925
926 Lists lines from new or modified text files in the change.
927
928 This is useful for doing line-by-line regex checks, like checking for
929 trailing whitespace.
930
931 Yields:
932 a 3 tuple:
933 the AffectedFile instance of the current file;
934 integer line number (1-based); and
935 the contents of the line as a string.
936 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000937 return _RightHandSideLinesImpl(
938 x for x in self.AffectedFiles(include_deletes=False)
939 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000940
941
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000942class SvnChange(Change):
943 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000944 scm = 'svn'
945 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000946
947 def _GetChangeLists(self):
948 """Get all change lists."""
949 if self._changelists == None:
950 previous_cwd = os.getcwd()
951 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000952 # Need to import here to avoid circular dependency.
953 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000954 self._changelists = gcl.GetModifiedFiles()
955 os.chdir(previous_cwd)
956 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000957
958 def GetAllModifiedFiles(self):
959 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000960 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000961 all_modified_files = []
962 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000963 all_modified_files.extend(
964 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000965 return all_modified_files
966
967 def GetModifiedFiles(self):
968 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000969 changelists = self._GetChangeLists()
970 return [os.path.join(self.RepositoryRoot(), f[1])
971 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000972
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000973 def AllFiles(self, root=None):
974 """List all files under source control in the repo."""
975 root = root or self.RepositoryRoot()
976 return subprocess.check_output(
977 ['svn', 'ls', '-R', '.'], cwd=root).splitlines()
978
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000979
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000980class GitChange(Change):
981 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000982 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000983
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000984 def AllFiles(self, root=None):
985 """List all files under source control in the repo."""
986 root = root or self.RepositoryRoot()
987 return subprocess.check_output(
988 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
989
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000990
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000991def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000992 """Finds all presubmit files that apply to a given set of source files.
993
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000994 If inherit-review-settings-ok is present right under root, looks for
995 PRESUBMIT.py in directories enclosing root.
996
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000997 Args:
998 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000999 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001000
1001 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001002 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001003 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001004 files = [normpath(os.path.join(root, f)) for f in files]
1005
1006 # List all the individual directories containing files.
1007 directories = set([os.path.dirname(f) for f in files])
1008
1009 # Ignore root if inherit-review-settings-ok is present.
1010 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1011 root = None
1012
1013 # Collect all unique directories that may contain PRESUBMIT.py.
1014 candidates = set()
1015 for directory in directories:
1016 while True:
1017 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001018 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001019 candidates.add(directory)
1020 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001021 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001022 parent_dir = os.path.dirname(directory)
1023 if parent_dir == directory:
1024 # We hit the system root directory.
1025 break
1026 directory = parent_dir
1027
1028 # Look for PRESUBMIT.py in all candidate directories.
1029 results = []
1030 for directory in sorted(list(candidates)):
1031 p = os.path.join(directory, 'PRESUBMIT.py')
1032 if os.path.isfile(p):
1033 results.append(p)
1034
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001035 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001036 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001037
1038
thestig@chromium.orgde243452009-10-06 21:02:56 +00001039class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001040 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001041 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +00001042 """Executes GetPreferredTrySlaves() from a single presubmit script.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001043
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001044 This will soon be deprecated and replaced by GetPreferredTryMasters().
thestig@chromium.orgde243452009-10-06 21:02:56 +00001045
1046 Args:
1047 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +00001048 presubmit_path: Project script to run.
1049 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001050
1051 Return:
1052 A list of try slaves.
1053 """
1054 context = {}
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001055 main_path = os.getcwd()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001056 try:
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001057 os.chdir(os.path.dirname(presubmit_path))
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001058 exec script_text in context
1059 except Exception, e:
1060 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001061 finally:
1062 os.chdir(main_path)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001063
1064 function_name = 'GetPreferredTrySlaves'
1065 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001066 get_preferred_try_slaves = context[function_name]
1067 function_info = inspect.getargspec(get_preferred_try_slaves)
1068 if len(function_info[0]) == 1:
1069 result = get_preferred_try_slaves(project)
1070 elif len(function_info[0]) == 2:
1071 result = get_preferred_try_slaves(project, change)
1072 else:
1073 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +00001074 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001075 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +00001076 'Presubmit functions must return a list, got a %s instead: %s' %
1077 (type(result), str(result)))
1078 for item in result:
stip@chromium.org68e04192013-11-04 22:14:38 +00001079 if isinstance(item, basestring):
1080 # Old-style ['bot'] format.
1081 botname = item
1082 elif isinstance(item, tuple):
1083 # New-style [('bot', set(['tests']))] format.
1084 botname = item[0]
1085 else:
1086 raise PresubmitFailure('PRESUBMIT.py returned invalid tryslave/test'
1087 ' format.')
1088
1089 if botname != botname.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001090 raise PresubmitFailure(
1091 'Try slave names cannot start/end with whitespace')
stip@chromium.org68e04192013-11-04 22:14:38 +00001092 if ',' in botname:
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +00001093 raise PresubmitFailure(
stip@chromium.org68e04192013-11-04 22:14:38 +00001094 'Do not use \',\' separated builder or test names: %s' % botname)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001095 else:
1096 result = []
stip@chromium.org5ca27622013-12-18 17:44:58 +00001097
1098 def valid_oldstyle(result):
1099 return all(isinstance(i, basestring) for i in result)
1100
1101 def valid_newstyle(result):
1102 return (all(isinstance(i, tuple) for i in result) and
1103 all(len(i) == 2 for i in result) and
1104 all(isinstance(i[0], basestring) for i in result) and
1105 all(isinstance(i[1], set) for i in result)
1106 )
1107
1108 # Ensure it's either all old-style or all new-style.
1109 if not valid_oldstyle(result) and not valid_newstyle(result):
1110 raise PresubmitFailure(
1111 'PRESUBMIT.py returned invalid trybot specification!')
1112
thestig@chromium.orgde243452009-10-06 21:02:56 +00001113 return result
1114
1115
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001116class GetTryMastersExecuter(object):
1117 @staticmethod
1118 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1119 """Executes GetPreferredTryMasters() from a single presubmit script.
1120
1121 Args:
1122 script_text: The text of the presubmit script.
1123 presubmit_path: Project script to run.
1124 project: Project name to pass to presubmit script for bot selection.
1125
1126 Return:
1127 A map of try masters to map of builders to set of tests.
1128 """
1129 context = {}
1130 try:
1131 exec script_text in context
1132 except Exception, e:
1133 raise PresubmitFailure('"%s" had an exception.\n%s'
1134 % (presubmit_path, e))
1135
1136 function_name = 'GetPreferredTryMasters'
1137 if function_name not in context:
1138 return {}
1139 get_preferred_try_masters = context[function_name]
1140 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1141 raise PresubmitFailure(
1142 'Expected function "GetPreferredTryMasters" to take two arguments.')
1143 return get_preferred_try_masters(project, change)
1144
1145
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001146def DoGetTrySlaves(change,
1147 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001148 repository_root,
1149 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +00001150 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001151 verbose,
1152 output_stream):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001153 """Get the list of try servers from the presubmit scripts (deprecated).
thestig@chromium.orgde243452009-10-06 21:02:56 +00001154
1155 Args:
1156 changed_files: List of modified files.
1157 repository_root: The repository root.
1158 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +00001159 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001160 verbose: Prints debug info.
1161 output_stream: A stream to write debug output to.
1162
1163 Return:
1164 List of try slaves
1165 """
1166 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1167 if not presubmit_files and verbose:
mdempsky@chromium.orgd59e7612014-03-05 19:55:56 +00001168 output_stream.write("Warning, no PRESUBMIT.py found.\n")
thestig@chromium.orgde243452009-10-06 21:02:56 +00001169 results = []
1170 executer = GetTrySlavesExecuter()
stip@chromium.org5ca27622013-12-18 17:44:58 +00001171
thestig@chromium.orgde243452009-10-06 21:02:56 +00001172 if default_presubmit:
1173 if verbose:
1174 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001175 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001176 results.extend(executer.ExecPresubmitScript(
1177 default_presubmit, fake_path, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001178 for filename in presubmit_files:
1179 filename = os.path.abspath(filename)
1180 if verbose:
1181 output_stream.write("Running %s\n" % filename)
1182 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001183 presubmit_script = gclient_utils.FileRead(filename, 'rU')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001184 results.extend(executer.ExecPresubmitScript(
1185 presubmit_script, filename, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001186
stip@chromium.org5ca27622013-12-18 17:44:58 +00001187
1188 slave_dict = {}
1189 old_style = filter(lambda x: isinstance(x, basestring), results)
1190 new_style = filter(lambda x: isinstance(x, tuple), results)
1191
1192 for result in new_style:
1193 slave_dict.setdefault(result[0], set()).update(result[1])
1194 slaves = list(slave_dict.items())
1195
1196 slaves.extend(set(old_style))
stip@chromium.org68e04192013-11-04 22:14:38 +00001197
thestig@chromium.orgde243452009-10-06 21:02:56 +00001198 if slaves and verbose:
stip@chromium.org5ca27622013-12-18 17:44:58 +00001199 output_stream.write(', '.join((str(x) for x in slaves)))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001200 output_stream.write('\n')
1201 return slaves
1202
1203
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001204def _MergeMasters(masters1, masters2):
1205 """Merges two master maps. Merges also the tests of each builder."""
1206 result = {}
1207 for (master, builders) in itertools.chain(masters1.iteritems(),
1208 masters2.iteritems()):
1209 new_builders = result.setdefault(master, {})
1210 for (builder, tests) in builders.iteritems():
1211 new_builders.setdefault(builder, set([])).update(tests)
1212 return result
1213
1214
1215def DoGetTryMasters(change,
1216 changed_files,
1217 repository_root,
1218 default_presubmit,
1219 project,
1220 verbose,
1221 output_stream):
1222 """Get the list of try masters from the presubmit scripts.
1223
1224 Args:
1225 changed_files: List of modified files.
1226 repository_root: The repository root.
1227 default_presubmit: A default presubmit script to execute in any case.
1228 project: Optional name of a project used in selecting trybots.
1229 verbose: Prints debug info.
1230 output_stream: A stream to write debug output to.
1231
1232 Return:
1233 Map of try masters to map of builders to set of tests.
1234 """
1235 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1236 if not presubmit_files and verbose:
1237 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1238 results = {}
1239 executer = GetTryMastersExecuter()
1240
1241 if default_presubmit:
1242 if verbose:
1243 output_stream.write("Running default presubmit script.\n")
1244 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1245 results = _MergeMasters(results, executer.ExecPresubmitScript(
1246 default_presubmit, fake_path, project, change))
1247 for filename in presubmit_files:
1248 filename = os.path.abspath(filename)
1249 if verbose:
1250 output_stream.write("Running %s\n" % filename)
1251 # Accept CRLF presubmit script.
1252 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1253 results = _MergeMasters(results, executer.ExecPresubmitScript(
1254 presubmit_script, filename, project, change))
1255
1256 # Make sets to lists again for later JSON serialization.
1257 for builders in results.itervalues():
1258 for builder in builders:
1259 builders[builder] = list(builders[builder])
1260
1261 if results and verbose:
1262 output_stream.write('%s\n' % str(results))
1263 return results
1264
1265
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001266class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001267 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001268 """
1269 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001270 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001271 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001272 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001273 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001274 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001275 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001276 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001277 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001278
1279 def ExecPresubmitScript(self, script_text, presubmit_path):
1280 """Executes a single presubmit script.
1281
1282 Args:
1283 script_text: The text of the presubmit script.
1284 presubmit_path: The path to the presubmit file (this will be reported via
1285 input_api.PresubmitLocalPath()).
1286
1287 Return:
1288 A list of result objects, empty if no problems.
1289 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001290
chase@chromium.org8e416c82009-10-06 04:30:44 +00001291 # Change to the presubmit file's directory to support local imports.
1292 main_path = os.getcwd()
1293 os.chdir(os.path.dirname(presubmit_path))
1294
1295 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001296 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001297 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001298 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001299 try:
1300 exec script_text in context
1301 except Exception, e:
1302 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001303
1304 # These function names must change if we make substantial changes to
1305 # the presubmit API that are not backwards compatible.
1306 if self.committing:
1307 function_name = 'CheckChangeOnCommit'
1308 else:
1309 function_name = 'CheckChangeOnUpload'
1310 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001311 context['__args'] = (input_api, OutputApi(self.committing))
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001312 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001313 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001314 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001315 if not (isinstance(result, types.TupleType) or
1316 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001317 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001318 'Presubmit functions must return a tuple or list')
1319 for item in result:
1320 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001321 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001322 'All presubmit results must be of types derived from '
1323 'output_api.PresubmitResult')
1324 else:
1325 result = () # no error since the script doesn't care about current event.
1326
chase@chromium.org8e416c82009-10-06 04:30:44 +00001327 # Return the process to the original working directory.
1328 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001329 return result
1330
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001331
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001332def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001333 committing,
1334 verbose,
1335 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001336 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001337 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001338 may_prompt,
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001339 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001340 """Runs all presubmit checks that apply to the files in the change.
1341
1342 This finds all PRESUBMIT.py files in directories enclosing the files in the
1343 change (up to the repository root) and calls the relevant entrypoint function
1344 depending on whether the change is being committed or uploaded.
1345
1346 Prints errors, warnings and notifications. Prompts the user for warnings
1347 when needed.
1348
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.
1352 verbose: Prints debug info.
1353 output_stream: A stream to write output from presubmit tests to.
1354 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001355 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001356 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001357 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001358
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001359 Warning:
1360 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1361 SHOULD be sys.stdin.
1362
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001363 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001364 A PresubmitOutput object. Use output.should_continue() to figure out
1365 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001366 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001367 old_environ = os.environ
1368 try:
1369 # Make sure python subprocesses won't generate .pyc files.
1370 os.environ = os.environ.copy()
1371 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001372
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001373 output = PresubmitOutput(input_stream, output_stream)
1374 if committing:
1375 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001376 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001377 output.write("Running presubmit upload checks ...\n")
1378 start_time = time.time()
1379 presubmit_files = ListRelevantPresubmitFiles(
1380 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1381 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001382 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001383 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001384 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001385 if default_presubmit:
1386 if verbose:
1387 output.write("Running default presubmit script.\n")
1388 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1389 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1390 for filename in presubmit_files:
1391 filename = os.path.abspath(filename)
1392 if verbose:
1393 output.write("Running %s\n" % filename)
1394 # Accept CRLF presubmit script.
1395 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1396 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001397
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001398 errors = []
1399 notifications = []
1400 warnings = []
1401 for result in results:
1402 if result.fatal:
1403 errors.append(result)
1404 elif result.should_prompt:
1405 warnings.append(result)
1406 else:
1407 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001408
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001409 output.write('\n')
1410 for name, items in (('Messages', notifications),
1411 ('Warnings', warnings),
1412 ('ERRORS', errors)):
1413 if items:
1414 output.write('** Presubmit %s **\n' % name)
1415 for item in items:
1416 item.handle(output)
1417 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001418
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001419 total_time = time.time() - start_time
1420 if total_time > 1.0:
1421 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001422
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001423 if not errors:
1424 if not warnings:
1425 output.write('Presubmit checks passed.\n')
1426 elif may_prompt:
1427 output.prompt_yes_no('There were presubmit warnings. '
1428 'Are you sure you wish to continue? (y/N): ')
1429 else:
1430 output.fail()
1431
1432 global _ASKED_FOR_FEEDBACK
1433 # Ask for feedback one time out of 5.
1434 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001435 output.write(
1436 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1437 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1438 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001439 _ASKED_FOR_FEEDBACK = True
1440 return output
1441 finally:
1442 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001443
1444
1445def ScanSubDirs(mask, recursive):
1446 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001447 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001448 else:
1449 results = []
1450 for root, dirs, files in os.walk('.'):
1451 if '.svn' in dirs:
1452 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001453 if '.git' in dirs:
1454 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001455 for name in files:
1456 if fnmatch.fnmatch(name, mask):
1457 results.append(os.path.join(root, name))
1458 return results
1459
1460
1461def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001462 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001463 files = []
1464 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001465 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001466 return files
1467
1468
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001469def load_files(options, args):
1470 """Tries to determine the SCM."""
1471 change_scm = scm.determine_scm(options.root)
1472 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001473 if args:
1474 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001475 if change_scm == 'svn':
1476 change_class = SvnChange
1477 if not files:
1478 files = scm.SVN.CaptureStatus([], options.root)
1479 elif change_scm == 'git':
1480 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001481 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001482 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001483 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001484 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001485 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1486 if not files:
1487 return None, None
1488 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001489 return change_class, files
1490
1491
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001492class NonexistantCannedCheckFilter(Exception):
1493 pass
1494
1495
1496@contextlib.contextmanager
1497def canned_check_filter(method_names):
1498 filtered = {}
1499 try:
1500 for method_name in method_names:
1501 if not hasattr(presubmit_canned_checks, method_name):
1502 raise NonexistantCannedCheckFilter(method_name)
1503 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1504 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1505 yield
1506 finally:
1507 for name, method in filtered.iteritems():
1508 setattr(presubmit_canned_checks, name, method)
1509
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001510
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001511def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001512 """Runs an external program, potentially from a child process created by the
1513 multiprocessing module.
1514
1515 multiprocessing needs a top level function with a single argument.
1516 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001517 cmd_data.kwargs['stdout'] = subprocess.PIPE
1518 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1519 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001520 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001521 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001522 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001523 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001524 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001525 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001526 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1527 if code != 0:
1528 return cmd_data.message(
1529 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1530 if cmd_data.info:
1531 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001532
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001533
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001534def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001535 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001536 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001537 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001538 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001539 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1540 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001541 parser.add_option("-r", "--recursive", action="store_true",
1542 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001543 parser.add_option("-v", "--verbose", action="count", default=0,
1544 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001545 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001546 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001547 parser.add_option("--description", default='')
1548 parser.add_option("--issue", type='int', default=0)
1549 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001550 parser.add_option("--root", default=os.getcwd(),
1551 help="Search for PRESUBMIT.py up to this directory. "
1552 "If inherit-review-settings-ok is present in this "
1553 "directory, parent directories up to the root file "
1554 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001555 parser.add_option("--upstream",
1556 help="Git only: the base ref or upstream branch against "
1557 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001558 parser.add_option("--default_presubmit")
1559 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001560 parser.add_option("--skip_canned", action='append', default=[],
1561 help="A list of checks to skip which appear in "
1562 "presubmit_canned_checks. Can be provided multiple times "
1563 "to skip multiple canned checks.")
maruel@chromium.org239f4112011-06-03 20:08:23 +00001564 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1565 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1566 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001567 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1568 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001569 # These are for OAuth2 authentication for bots. See also apply_issue.py
1570 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1571 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1572
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001573 parser.add_option("--trybot-json",
1574 help="Output trybot information to the file specified.")
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001575 options, args = parser.parse_args(argv)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001576
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001577 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001578 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001579 elif options.verbose:
1580 logging.basicConfig(level=logging.INFO)
1581 else:
1582 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001583
1584 if options.rietveld_email and options.rietveld_email_file:
1585 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1586 "can be passed to this program.")
1587 if options.rietveld_private_key_file and options.rietveld_password:
1588 parser.error("Only one of --rietveld_private_key_file or "
1589 "--rietveld_password can be passed to this program.")
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001590
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001591 if options.rietveld_email_file:
1592 with open(options.rietveld_email_file, "rb") as f:
1593 options.rietveld_email = f.read().strip()
1594
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001595 change_class, files = load_files(options, args)
1596 if not change_class:
1597 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001598 logging.info('Found %d file(s).' % len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001599
maruel@chromium.org239f4112011-06-03 20:08:23 +00001600 rietveld_obj = None
1601 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001602 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001603 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001604 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1605 options.rietveld_url,
1606 options.rietveld_email,
1607 options.rietveld_private_key_file)
1608 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001609 rietveld_obj = rietveld.CachingRietveld(
1610 options.rietveld_url,
1611 options.rietveld_email,
1612 options.rietveld_password)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001613 if options.rietveld_fetch:
1614 assert options.issue
1615 props = rietveld_obj.get_issue_properties(options.issue, False)
1616 options.author = props['owner_email']
1617 options.description = props['description']
1618 logging.info('Got author: "%s"', options.author)
1619 logging.info('Got description: """\n%s\n"""', options.description)
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001620 if options.trybot_json:
1621 with open(options.trybot_json, 'w') as f:
1622 # Python's sets aren't JSON-encodable, so we convert them to lists here.
1623 class SetEncoder(json.JSONEncoder):
1624 # pylint: disable=E0202
1625 def default(self, obj):
1626 if isinstance(obj, set):
1627 return sorted(obj)
1628 return json.JSONEncoder.default(self, obj)
1629 change = change_class(options.name,
1630 options.description,
1631 options.root,
1632 files,
1633 options.issue,
1634 options.patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001635 options.author,
1636 upstream=options.upstream)
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001637 trybots = DoGetTrySlaves(
1638 change,
1639 change.LocalPaths(),
1640 change.RepositoryRoot(),
1641 None,
1642 None,
1643 options.verbose,
1644 sys.stdout)
1645 json.dump(trybots, f, cls=SetEncoder)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001646 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001647 with canned_check_filter(options.skip_canned):
1648 results = DoPresubmitChecks(
1649 change_class(options.name,
1650 options.description,
1651 options.root,
1652 files,
1653 options.issue,
1654 options.patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001655 options.author,
1656 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001657 options.commit,
1658 options.verbose,
1659 sys.stdout,
1660 sys.stdin,
1661 options.default_presubmit,
1662 options.may_prompt,
1663 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001664 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001665 except NonexistantCannedCheckFilter, e:
1666 print >> sys.stderr, (
1667 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1668 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001669 except PresubmitFailure, e:
1670 print >> sys.stderr, e
1671 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1672 print >> sys.stderr, 'If all fails, contact maruel@'
1673 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001674
1675
1676if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001677 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001678 sys.exit(Main(None))