blob: 9c93ab85ae625b81157605d63e1a2434339a6696 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
2# Copyright (c) 2011 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
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00009__version__ = '1.6.1'
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
15import cPickle # Exposed through the API.
16import cStringIO # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000017import fnmatch
18import glob
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000019import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000020import marshal # Exposed through the API.
21import optparse
22import os # Somewhat exposed through the API.
23import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000024import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000025import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import sys # Parts exposed through API.
27import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000028import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000029import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000030import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000031import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000033from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000035try:
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000036 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000037except ImportError:
38 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000039 import json # pylint: disable=F0401
40 except ImportError:
maruel@chromium.org59c7ba62010-03-20 00:13:07 +000041 # Import the one included in depot_tools.
maruel@chromium.orgd08cb1e2010-03-20 00:25:19 +000042 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000043 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000044
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000045# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000046import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000047import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000048import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000049import presubmit_canned_checks
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000050import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000051import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052
53
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000054# Ask for feedback only once in program lifetime.
55_ASKED_FOR_FEEDBACK = False
56
57
maruel@chromium.org899e1c12011-04-07 17:03:18 +000058class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000059 pass
60
61
62def normpath(path):
63 '''Version of os.path.normpath that also changes backward slashes to
64 forward slashes when not running on Windows.
65 '''
66 # This is safe to always do because the Windows version of os.path.normpath
67 # will replace forward slashes with backward slashes.
68 path = path.replace(os.sep, '/')
69 return os.path.normpath(path)
70
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000071
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000072def _RightHandSideLinesImpl(affected_files):
73 """Implements RightHandSideLines for InputApi and GclChange."""
74 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000075 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000076 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000077 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000078
79
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000080class PresubmitOutput(object):
81 def __init__(self, input_stream=None, output_stream=None):
82 self.input_stream = input_stream
83 self.output_stream = output_stream
84 self.reviewers = []
85 self.written_output = []
86 self.error_count = 0
87
88 def prompt_yes_no(self, prompt_string):
89 self.write(prompt_string)
90 if self.input_stream:
91 response = self.input_stream.readline().strip().lower()
92 if response not in ('y', 'yes'):
93 self.fail()
94 else:
95 self.fail()
96
97 def fail(self):
98 self.error_count += 1
99
100 def should_continue(self):
101 return not self.error_count
102
103 def write(self, s):
104 self.written_output.append(s)
105 if self.output_stream:
106 self.output_stream.write(s)
107
108 def getvalue(self):
109 return ''.join(self.written_output)
110
111
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000112class OutputApi(object):
113 """This class (more like a module) gets passed to presubmit scripts so that
114 they can specify various types of results.
115 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000116 class PresubmitResult(object):
117 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000118 fatal = False
119 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000120
121 def __init__(self, message, items=None, long_text=''):
122 """
123 message: A short one-line message to indicate errors.
124 items: A list of short strings to indicate where errors occurred.
125 long_text: multi-line text output, e.g. from another tool
126 """
127 self._message = message
128 self._items = []
129 if items:
130 self._items = items
131 self._long_text = long_text.rstrip()
132
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000133 def handle(self, output):
134 output.write(self._message)
135 output.write('\n')
maruel@chromium.org35625c72011-03-23 17:34:02 +0000136 for index, item in enumerate(self._items):
137 output.write(' ')
138 # Write separately in case it's unicode.
maruel@chromium.org604a5892011-03-23 23:55:48 +0000139 output.write(str(item))
maruel@chromium.org35625c72011-03-23 17:34:02 +0000140 if index < len(self._items) - 1:
141 output.write(' \\')
142 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000143 if self._long_text:
maruel@chromium.org35625c72011-03-23 17:34:02 +0000144 output.write('\n***************\n')
145 # Write separately in case it's unicode.
146 output.write(self._long_text)
147 output.write('\n***************\n')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000148 if self.fatal:
149 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000150
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000151 class PresubmitAddReviewers(PresubmitResult):
152 """Add some suggested reviewers to the change."""
153 def __init__(self, reviewers):
154 super(OutputApi.PresubmitAddReviewers, self).__init__('')
155 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000156
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000157 def handle(self, output):
158 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000159
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000160 class PresubmitError(PresubmitResult):
161 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000162 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000163
164 class PresubmitPromptWarning(PresubmitResult):
165 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000166 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000167
168 class PresubmitNotifyResult(PresubmitResult):
169 """Just print something to the screen -- but it's not even a warning."""
170 pass
171
172 class MailTextResult(PresubmitResult):
173 """A warning that should be included in the review request email."""
174 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000175 super(OutputApi.MailTextResult, self).__init__()
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000176 raise NotImplementedError()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000177
178
179class InputApi(object):
180 """An instance of this object is passed to presubmit scripts so they can
181 know stuff about the change they're looking at.
182 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000183 # Method could be a function
184 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000185
maruel@chromium.org3410d912009-06-09 20:56:16 +0000186 # File extensions that are considered source files from a style guide
187 # perspective. Don't modify this list from a presubmit script!
188 DEFAULT_WHITE_LIST = (
189 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000190 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
191 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000192 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000193 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000194 # No extension at all, note that ALL CAPS files are black listed in
195 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000196 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000197 # Other
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000198 r".+\.java$", r".+\.mk$", r".+\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000199 )
200
201 # Path regexp that should be excluded from being considered containing source
202 # files. Don't modify this list from a presubmit script!
203 DEFAULT_BLACK_LIST = (
204 r".*\bexperimental[\\\/].*",
205 r".*\bthird_party[\\\/].*",
206 # Output directories (just in case)
207 r".*\bDebug[\\\/].*",
208 r".*\bRelease[\\\/].*",
209 r".*\bxcodebuild[\\\/].*",
210 r".*\bsconsbuild[\\\/].*",
211 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000212 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000213 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000214 r"(|.*[\\\/])\.git[\\\/].*",
215 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000216 )
217
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000218 def __init__(self, change, presubmit_path, is_committing, tbr,
219 rietveld, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000220 """Builds an InputApi object.
221
222 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000223 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000224 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000225 is_committing: True if the change is about to be committed.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000226 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000227 rietveld: rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000228 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000229 # Version number of the presubmit_support script.
230 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000231 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000232 self.is_committing = is_committing
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000233 self.tbr = tbr
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000234 self.rietveld = rietveld
235 # TBD
236 self.host_url = 'http://codereview.chromium.org'
237 if self.rietveld:
238 self.host_url = rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000239
240 # We expose various modules and functions as attributes of the input_api
241 # so that presubmit scripts don't have to import them.
242 self.basename = os.path.basename
243 self.cPickle = cPickle
244 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000245 self.json = json
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000246 self.os_listdir = os.listdir
247 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000248 self.os_path = os.path
249 self.pickle = pickle
250 self.marshal = marshal
251 self.re = re
252 self.subprocess = subprocess
253 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000254 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000255 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000256 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000257 self.urllib2 = urllib2
258
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000259 # To easily fork python.
260 self.python_executable = sys.executable
261 self.environ = os.environ
262
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000263 # InputApi.platform is the platform you're currently running on.
264 self.platform = sys.platform
265
266 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000267 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000268
269 # We carry the canned checks so presubmit scripts can easily use them.
270 self.canned_checks = presubmit_canned_checks
271
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000272 # TODO(dpranke): figure out a list of all approved owners for a repo
273 # in order to be able to handle wildcard OWNERS files?
274 self.owners_db = owners.Database(change.RepositoryRoot(),
275 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000276 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000277
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000278 def PresubmitLocalPath(self):
279 """Returns the local path of the presubmit script currently being run.
280
281 This is useful if you don't want to hard-code absolute paths in the
282 presubmit script. For example, It can be used to find another file
283 relative to the PRESUBMIT.py script, so the whole tree can be branched and
284 the presubmit script still works, without editing its content.
285 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000286 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000287
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000288 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000289 """Translate a depot path to a local path (relative to client root).
290
291 Args:
292 Depot path as a string.
293
294 Returns:
295 The local path of the depot path under the user's current client, or None
296 if the file is not mapped.
297
298 Remember to check for the None case and show an appropriate error!
299 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000300 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000301 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000302 return local_path
303
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000304 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000305 """Translate a local path to a depot path.
306
307 Args:
308 Local path (relative to current directory, or absolute) as a string.
309
310 Returns:
311 The depot path (SVN URL) of the file if mapped, otherwise None.
312 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000313 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000314 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000315 return depot_path
316
sail@chromium.org5538e022011-05-12 17:53:16 +0000317 def AffectedFiles(self, include_dirs=False, include_deletes=True,
318 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000319 """Same as input_api.change.AffectedFiles() except only lists files
320 (and optionally directories) in the same directory as the current presubmit
321 script, or subdirectories thereof.
322 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000323 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000324 if len(dir_with_slash) == 1:
325 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000326
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000327 return filter(
328 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000329 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000330
331 def LocalPaths(self, include_dirs=False):
332 """Returns local paths of input_api.AffectedFiles()."""
333 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
334
335 def AbsoluteLocalPaths(self, include_dirs=False):
336 """Returns absolute local paths of input_api.AffectedFiles()."""
337 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
338
339 def ServerPaths(self, include_dirs=False):
340 """Returns server paths of input_api.AffectedFiles()."""
341 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
342
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000343 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000344 """Same as input_api.change.AffectedTextFiles() except only lists files
345 in the same directory as the current presubmit script, or subdirectories
346 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000347 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000348 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000349 warn("AffectedTextFiles(include_deletes=%s)"
350 " is deprecated and ignored" % str(include_deletes),
351 category=DeprecationWarning,
352 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000353 return filter(lambda x: x.IsTextFile(),
354 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000355
maruel@chromium.org3410d912009-06-09 20:56:16 +0000356 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
357 """Filters out files that aren't considered "source file".
358
359 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
360 and InputApi.DEFAULT_BLACK_LIST is used respectively.
361
362 The lists will be compiled as regular expression and
363 AffectedFile.LocalPath() needs to pass both list.
364
365 Note: Copy-paste this function to suit your needs or use a lambda function.
366 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000367 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000368 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000369 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000370 if self.re.match(item, local_path):
371 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000372 return True
373 return False
374 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
375 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
376
377 def AffectedSourceFiles(self, source_file):
378 """Filter the list of AffectedTextFiles by the function source_file.
379
380 If source_file is None, InputApi.FilterSourceFile() is used.
381 """
382 if not source_file:
383 source_file = self.FilterSourceFile
384 return filter(source_file, self.AffectedTextFiles())
385
386 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000387 """An iterator over all text lines in "new" version of changed files.
388
389 Only lists lines from new or modified text files in the change that are
390 contained by the directory of the currently executing presubmit script.
391
392 This is useful for doing line-by-line regex checks, like checking for
393 trailing whitespace.
394
395 Yields:
396 a 3 tuple:
397 the AffectedFile instance of the current file;
398 integer line number (1-based); and
399 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000400
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000401 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000402 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000403 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000404 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000405
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000406 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000407 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000408
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000409 Deny reading anything outside the repository.
410 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000411 if isinstance(file_item, AffectedFile):
412 file_item = file_item.AbsoluteLocalPath()
413 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000414 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000415 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000416
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000417
418class AffectedFile(object):
419 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000420 # Method could be a function
421 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000422 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000423 self._path = path
424 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000425 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000426 self._is_directory = None
427 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000428 self._cached_changed_contents = None
429 self._cached_new_contents = None
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000430 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000431
432 def ServerPath(self):
433 """Returns a path string that identifies the file in the SCM system.
434
435 Returns the empty string if the file does not exist in SCM.
436 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000437 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000438
439 def LocalPath(self):
440 """Returns the path of this file on the local disk relative to client root.
441 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000442 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000443
444 def AbsoluteLocalPath(self):
445 """Returns the absolute path of this file on the local disk.
446 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000447 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000448
449 def IsDirectory(self):
450 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000451 if self._is_directory is None:
452 path = self.AbsoluteLocalPath()
453 self._is_directory = (os.path.exists(path) and
454 os.path.isdir(path))
455 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000456
457 def Action(self):
458 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000459 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
460 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000461 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000462
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000463 def Property(self, property_name):
464 """Returns the specified SCM property of this file, or None if no such
465 property.
466 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000467 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000468
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000469 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000470 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000471
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000472 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000473 raise NotImplementedError() # Implement when needed
474
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000475 def NewContents(self):
476 """Returns an iterator over the lines in the new version of file.
477
478 The new version is the file in the user's workspace, i.e. the "right hand
479 side".
480
481 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000482 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000483 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000484 if self._cached_new_contents is None:
485 self._cached_new_contents = []
486 if not self.IsDirectory():
487 try:
488 self._cached_new_contents = gclient_utils.FileRead(
489 self.AbsoluteLocalPath(), 'rU').splitlines()
490 except IOError:
491 pass # File not found? That's fine; maybe it was deleted.
492 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000493
494 def OldContents(self):
495 """Returns an iterator over the lines in the old version of file.
496
497 The old version is the file in depot, i.e. the "left hand side".
498 """
499 raise NotImplementedError() # Implement when needed
500
501 def OldFileTempPath(self):
502 """Returns the path on local disk where the old contents resides.
503
504 The old version is the file in depot, i.e. the "left hand side".
505 This is a read-only cached copy of the old contents. *DO NOT* try to
506 modify this file.
507 """
508 raise NotImplementedError() # Implement if/when needed.
509
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000510 def ChangedContents(self):
511 """Returns a list of tuples (line number, line text) of all new lines.
512
513 This relies on the scm diff output describing each changed code section
514 with a line of the form
515
516 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
517 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000518 if self._cached_changed_contents is not None:
519 return self._cached_changed_contents[:]
520 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000521 line_num = 0
522
523 if self.IsDirectory():
524 return []
525
526 for line in self.GenerateScmDiff().splitlines():
527 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
528 if m:
529 line_num = int(m.groups(1)[0])
530 continue
531 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000532 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000533 if not line.startswith('-'):
534 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000535 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000536
maruel@chromium.org5de13972009-06-10 18:16:06 +0000537 def __str__(self):
538 return self.LocalPath()
539
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000540 def GenerateScmDiff(self):
541 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000542
maruel@chromium.org58407af2011-04-12 23:15:57 +0000543
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000544class SvnAffectedFile(AffectedFile):
545 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000546 # Method 'NNN' is abstract in class 'NNN' but is not overridden
547 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000548
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000549 def __init__(self, *args, **kwargs):
550 AffectedFile.__init__(self, *args, **kwargs)
551 self._server_path = None
552 self._is_text_file = None
553
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000554 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000555 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000556 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000557 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000558 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000559
560 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000561 if self._is_directory is None:
562 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000563 if os.path.exists(path):
564 # Retrieve directly from the file system; it is much faster than
565 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000566 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000567 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000568 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000569 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000570 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000571
572 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000573 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000574 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000575 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000576 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000577
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000578 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000579 if self._is_text_file is None:
580 if self.Action() == 'D':
581 # A deleted file is not a text file.
582 self._is_text_file = False
583 elif self.IsDirectory():
584 self._is_text_file = False
585 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000586 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
587 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000588 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
589 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000590
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000591 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000592 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
593
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000594
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000595class GitAffectedFile(AffectedFile):
596 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000597 # Method 'NNN' is abstract in class 'NNN' but is not overridden
598 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000599
600 def __init__(self, *args, **kwargs):
601 AffectedFile.__init__(self, *args, **kwargs)
602 self._server_path = None
603 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000604
605 def ServerPath(self):
606 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000607 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000608 return self._server_path
609
610 def IsDirectory(self):
611 if self._is_directory is None:
612 path = self.AbsoluteLocalPath()
613 if os.path.exists(path):
614 # Retrieve directly from the file system; it is much faster than
615 # querying subversion, especially on Windows.
616 self._is_directory = os.path.isdir(path)
617 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000618 self._is_directory = False
619 return self._is_directory
620
621 def Property(self, property_name):
622 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000623 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000624 return self._properties[property_name]
625
626 def IsTextFile(self):
627 if self._is_text_file is None:
628 if self.Action() == 'D':
629 # A deleted file is not a text file.
630 self._is_text_file = False
631 elif self.IsDirectory():
632 self._is_text_file = False
633 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000634 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
635 return self._is_text_file
636
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000637 def GenerateScmDiff(self):
638 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000639
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000640
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000641class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000642 """Describe a change.
643
644 Used directly by the presubmit scripts to query the current change being
645 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000646
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000647 Instance members:
648 tags: Dictionnary of KEY=VALUE pairs found in the change description.
649 self.KEY: equivalent to tags['KEY']
650 """
651
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000652 _AFFECTED_FILES = AffectedFile
653
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000654 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000655 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000656 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000657 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000658
maruel@chromium.org58407af2011-04-12 23:15:57 +0000659 def __init__(
660 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000661 if files is None:
662 files = []
663 self._name = name
664 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000665 # Convert root into an absolute path.
666 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000667 self.issue = issue
668 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000669 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000670
671 # From the description text, build up a dictionary of key/value pairs
672 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000673 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000674 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000675 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000676 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000677 if m:
678 self.tags[m.group('key')] = m.group('value')
679 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000680 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000681
682 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000683 self._description_without_tags = (
684 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000685
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000686 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000687 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
688 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000689 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000690
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000691 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000692 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000693 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000694
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000695 def DescriptionText(self):
696 """Returns the user-entered changelist description, minus tags.
697
698 Any line in the user-provided description starting with e.g. "FOO="
699 (whitespace permitted before and around) is considered a tag line. Such
700 lines are stripped out of the description this function returns.
701 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000702 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000703
704 def FullDescriptionText(self):
705 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000706 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000707
708 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000709 """Returns the repository (checkout) root directory for this change,
710 as an absolute path.
711 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000712 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000713
714 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000715 """Return tags directly as attributes on the object."""
716 if not re.match(r"^[A-Z_]*$", attr):
717 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000718 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000719
sail@chromium.org5538e022011-05-12 17:53:16 +0000720 def AffectedFiles(self, include_dirs=False, include_deletes=True,
721 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000722 """Returns a list of AffectedFile instances for all files in the change.
723
724 Args:
725 include_deletes: If false, deleted files will be filtered out.
726 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000727 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000728
729 Returns:
730 [AffectedFile(path, action), AffectedFile(path, action)]
731 """
732 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000733 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000734 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000735 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000736
sail@chromium.org5538e022011-05-12 17:53:16 +0000737 affected = filter(file_filter, affected)
738
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000739 if include_deletes:
740 return affected
741 else:
742 return filter(lambda x: x.Action() != 'D', affected)
743
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000744 def AffectedTextFiles(self, include_deletes=None):
745 """Return a list of the existing text files in a change."""
746 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000747 warn("AffectedTextFiles(include_deletes=%s)"
748 " is deprecated and ignored" % str(include_deletes),
749 category=DeprecationWarning,
750 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000751 return filter(lambda x: x.IsTextFile(),
752 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000753
754 def LocalPaths(self, include_dirs=False):
755 """Convenience function."""
756 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
757
758 def AbsoluteLocalPaths(self, include_dirs=False):
759 """Convenience function."""
760 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
761
762 def ServerPaths(self, include_dirs=False):
763 """Convenience function."""
764 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
765
766 def RightHandSideLines(self):
767 """An iterator over all text lines in "new" version of changed files.
768
769 Lists lines from new or modified text files in the change.
770
771 This is useful for doing line-by-line regex checks, like checking for
772 trailing whitespace.
773
774 Yields:
775 a 3 tuple:
776 the AffectedFile instance of the current file;
777 integer line number (1-based); and
778 the contents of the line as a string.
779 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000780 return _RightHandSideLinesImpl(
781 x for x in self.AffectedFiles(include_deletes=False)
782 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000783
784
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000785class SvnChange(Change):
786 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000787 scm = 'svn'
788 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000789
790 def _GetChangeLists(self):
791 """Get all change lists."""
792 if self._changelists == None:
793 previous_cwd = os.getcwd()
794 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000795 # Need to import here to avoid circular dependency.
796 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000797 self._changelists = gcl.GetModifiedFiles()
798 os.chdir(previous_cwd)
799 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000800
801 def GetAllModifiedFiles(self):
802 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000803 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000804 all_modified_files = []
805 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000806 all_modified_files.extend(
807 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000808 return all_modified_files
809
810 def GetModifiedFiles(self):
811 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000812 changelists = self._GetChangeLists()
813 return [os.path.join(self.RepositoryRoot(), f[1])
814 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000815
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000816
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000817class GitChange(Change):
818 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000819 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000820
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000821
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000822def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000823 """Finds all presubmit files that apply to a given set of source files.
824
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000825 If inherit-review-settings-ok is present right under root, looks for
826 PRESUBMIT.py in directories enclosing root.
827
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000828 Args:
829 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000830 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000831
832 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000833 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000834 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000835 files = [normpath(os.path.join(root, f)) for f in files]
836
837 # List all the individual directories containing files.
838 directories = set([os.path.dirname(f) for f in files])
839
840 # Ignore root if inherit-review-settings-ok is present.
841 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
842 root = None
843
844 # Collect all unique directories that may contain PRESUBMIT.py.
845 candidates = set()
846 for directory in directories:
847 while True:
848 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000849 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000850 candidates.add(directory)
851 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000852 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000853 parent_dir = os.path.dirname(directory)
854 if parent_dir == directory:
855 # We hit the system root directory.
856 break
857 directory = parent_dir
858
859 # Look for PRESUBMIT.py in all candidate directories.
860 results = []
861 for directory in sorted(list(candidates)):
862 p = os.path.join(directory, 'PRESUBMIT.py')
863 if os.path.isfile(p):
864 results.append(p)
865
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000866 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000867 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000868
869
thestig@chromium.orgde243452009-10-06 21:02:56 +0000870class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000871 @staticmethod
bradnelson@google.com78230022011-05-24 18:55:19 +0000872 def ExecPresubmitScript(script_text, presubmit_path, project):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000873 """Executes GetPreferredTrySlaves() from a single presubmit script.
874
875 Args:
876 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000877 presubmit_path: Project script to run.
878 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000879
880 Return:
881 A list of try slaves.
882 """
883 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000884 try:
885 exec script_text in context
886 except Exception, e:
887 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000888
889 function_name = 'GetPreferredTrySlaves'
890 if function_name in context:
bradnelson@google.com78230022011-05-24 18:55:19 +0000891 try:
892 result = eval(function_name + '(' + repr(project) + ')', context)
893 except TypeError:
894 result = eval(function_name + '()', context)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000895 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000896 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000897 'Presubmit functions must return a list, got a %s instead: %s' %
898 (type(result), str(result)))
899 for item in result:
900 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000901 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000902 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000903 raise PresubmitFailure(
904 'Try slave names cannot start/end with whitespace')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000905 else:
906 result = []
907 return result
908
909
910def DoGetTrySlaves(changed_files,
911 repository_root,
912 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000913 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000914 verbose,
915 output_stream):
916 """Get the list of try servers from the presubmit scripts.
917
918 Args:
919 changed_files: List of modified files.
920 repository_root: The repository root.
921 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000922 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000923 verbose: Prints debug info.
924 output_stream: A stream to write debug output to.
925
926 Return:
927 List of try slaves
928 """
929 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
930 if not presubmit_files and verbose:
931 output_stream.write("Warning, no presubmit.py found.\n")
932 results = []
933 executer = GetTrySlavesExecuter()
934 if default_presubmit:
935 if verbose:
936 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000937 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000938 results += executer.ExecPresubmitScript(
939 default_presubmit, fake_path, project)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000940 for filename in presubmit_files:
941 filename = os.path.abspath(filename)
942 if verbose:
943 output_stream.write("Running %s\n" % filename)
944 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000945 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000946 results += executer.ExecPresubmitScript(
947 presubmit_script, filename, project)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000948
949 slaves = list(set(results))
950 if slaves and verbose:
951 output_stream.write(', '.join(slaves))
952 output_stream.write('\n')
953 return slaves
954
955
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000956class PresubmitExecuter(object):
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000957 def __init__(self, change, committing, tbr, rietveld, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000958 """
959 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000960 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000961 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000962 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000963 rietveld: rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000964 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000965 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000966 self.committing = committing
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000967 self.tbr = tbr
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000968 self.rietveld = rietveld
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000969 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000970
971 def ExecPresubmitScript(self, script_text, presubmit_path):
972 """Executes a single presubmit script.
973
974 Args:
975 script_text: The text of the presubmit script.
976 presubmit_path: The path to the presubmit file (this will be reported via
977 input_api.PresubmitLocalPath()).
978
979 Return:
980 A list of result objects, empty if no problems.
981 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000982
983 # Change to the presubmit file's directory to support local imports.
984 main_path = os.getcwd()
985 os.chdir(os.path.dirname(presubmit_path))
986
987 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000988 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000989 self.tbr, self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000990 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000991 try:
992 exec script_text in context
993 except Exception, e:
994 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000995
996 # These function names must change if we make substantial changes to
997 # the presubmit API that are not backwards compatible.
998 if self.committing:
999 function_name = 'CheckChangeOnCommit'
1000 else:
1001 function_name = 'CheckChangeOnUpload'
1002 if function_name in context:
1003 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001004 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001005 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001006 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001007 if not (isinstance(result, types.TupleType) or
1008 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001009 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010 'Presubmit functions must return a tuple or list')
1011 for item in result:
1012 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001013 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001014 'All presubmit results must be of types derived from '
1015 'output_api.PresubmitResult')
1016 else:
1017 result = () # no error since the script doesn't care about current event.
1018
chase@chromium.org8e416c82009-10-06 04:30:44 +00001019 # Return the process to the original working directory.
1020 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001021 return result
1022
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001023
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001024def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001025 committing,
1026 verbose,
1027 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001028 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001029 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001030 may_prompt,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001031 tbr,
1032 rietveld):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001033 """Runs all presubmit checks that apply to the files in the change.
1034
1035 This finds all PRESUBMIT.py files in directories enclosing the files in the
1036 change (up to the repository root) and calls the relevant entrypoint function
1037 depending on whether the change is being committed or uploaded.
1038
1039 Prints errors, warnings and notifications. Prompts the user for warnings
1040 when needed.
1041
1042 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001043 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001044 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1045 verbose: Prints debug info.
1046 output_stream: A stream to write output from presubmit tests to.
1047 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001048 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001049 may_prompt: Enable (y/n) questions on warning or error.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001050 tbr: was --tbr specified to skip any reviewer/owner checks?
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001051 rietveld: rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001052
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001053 Warning:
1054 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1055 SHOULD be sys.stdin.
1056
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001057 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001058 A PresubmitOutput object. Use output.should_continue() to figure out
1059 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001060 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001061 old_environ = os.environ
1062 try:
1063 # Make sure python subprocesses won't generate .pyc files.
1064 os.environ = os.environ.copy()
1065 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001066
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001067 output = PresubmitOutput(input_stream, output_stream)
1068 if committing:
1069 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001070 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001071 output.write("Running presubmit upload checks ...\n")
1072 start_time = time.time()
1073 presubmit_files = ListRelevantPresubmitFiles(
1074 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1075 if not presubmit_files and verbose:
1076 output.write("Warning, no presubmit.py found.\n")
1077 results = []
1078 executer = PresubmitExecuter(change, committing, tbr, rietveld, verbose)
1079 if default_presubmit:
1080 if verbose:
1081 output.write("Running default presubmit script.\n")
1082 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1083 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1084 for filename in presubmit_files:
1085 filename = os.path.abspath(filename)
1086 if verbose:
1087 output.write("Running %s\n" % filename)
1088 # Accept CRLF presubmit script.
1089 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1090 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001091
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001092 errors = []
1093 notifications = []
1094 warnings = []
1095 for result in results:
1096 if result.fatal:
1097 errors.append(result)
1098 elif result.should_prompt:
1099 warnings.append(result)
1100 else:
1101 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001102
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001103 output.write('\n')
1104 for name, items in (('Messages', notifications),
1105 ('Warnings', warnings),
1106 ('ERRORS', errors)):
1107 if items:
1108 output.write('** Presubmit %s **\n' % name)
1109 for item in items:
1110 item.handle(output)
1111 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001112
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001113 total_time = time.time() - start_time
1114 if total_time > 1.0:
1115 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001116
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001117 if not errors:
1118 if not warnings:
1119 output.write('Presubmit checks passed.\n')
1120 elif may_prompt:
1121 output.prompt_yes_no('There were presubmit warnings. '
1122 'Are you sure you wish to continue? (y/N): ')
1123 else:
1124 output.fail()
1125
1126 global _ASKED_FOR_FEEDBACK
1127 # Ask for feedback one time out of 5.
1128 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1129 output.write("Was the presubmit check useful? Please send feedback "
1130 "& hate mail to maruel@chromium.org!\n")
1131 _ASKED_FOR_FEEDBACK = True
1132 return output
1133 finally:
1134 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001135
1136
1137def ScanSubDirs(mask, recursive):
1138 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001139 return [x for x in glob.glob(mask) if '.svn' not in x and '.git' not in x]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001140 else:
1141 results = []
1142 for root, dirs, files in os.walk('.'):
1143 if '.svn' in dirs:
1144 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001145 if '.git' in dirs:
1146 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001147 for name in files:
1148 if fnmatch.fnmatch(name, mask):
1149 results.append(os.path.join(root, name))
1150 return results
1151
1152
1153def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001154 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001155 files = []
1156 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001157 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001158 return files
1159
1160
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001161def load_files(options, args):
1162 """Tries to determine the SCM."""
1163 change_scm = scm.determine_scm(options.root)
1164 files = []
1165 if change_scm == 'svn':
1166 change_class = SvnChange
1167 status_fn = scm.SVN.CaptureStatus
1168 elif change_scm == 'git':
1169 change_class = GitChange
1170 status_fn = scm.GIT.CaptureStatus
1171 else:
1172 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1173 if not args:
1174 return None, None
1175 change_class = Change
1176 if args:
1177 files = ParseFiles(args, options.recursive)
1178 else:
1179 # Grab modified files.
1180 files = status_fn([options.root])
1181 return change_class, files
1182
1183
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001184def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001185 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001186 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001187 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001188 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001189 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1190 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001191 parser.add_option("-r", "--recursive", action="store_true",
1192 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001193 parser.add_option("-v", "--verbose", action="count", default=0,
1194 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001195 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001196 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001197 parser.add_option("--description", default='')
1198 parser.add_option("--issue", type='int', default=0)
1199 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001200 parser.add_option("--root", default=os.getcwd(),
1201 help="Search for PRESUBMIT.py up to this directory. "
1202 "If inherit-review-settings-ok is present in this "
1203 "directory, parent directories up to the root file "
1204 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001205 parser.add_option("--default_presubmit")
1206 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001207 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001208 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001209 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001210 elif options.verbose:
1211 logging.basicConfig(level=logging.INFO)
1212 else:
1213 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001214 change_class, files = load_files(options, args)
1215 if not change_class:
1216 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001217 logging.info('Found %d file(s).' % len(files))
1218 try:
1219 results = DoPresubmitChecks(
1220 change_class(options.name,
1221 options.description,
1222 options.root,
1223 files,
1224 options.issue,
maruel@chromium.org58407af2011-04-12 23:15:57 +00001225 options.patchset,
1226 options.author),
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001227 options.commit,
1228 options.verbose,
1229 sys.stdout,
1230 sys.stdin,
1231 options.default_presubmit,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001232 options.may_prompt,
1233 False,
1234 None)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001235 return not results.should_continue()
1236 except PresubmitFailure, e:
1237 print >> sys.stderr, e
1238 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1239 print >> sys.stderr, 'If all fails, contact maruel@'
1240 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001241
1242
1243if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001244 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001245 sys.exit(Main(None))