blob: bef76e9289f550f5503171ce894c724fb7d06177 [file] [log] [blame]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001#!/usr/bin/python
2# Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
3# 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.orgb7d46902009-06-10 14:12:10 +00009__version__ = '1.3.2'
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.
17import exceptions
18import fnmatch
19import glob
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000020import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000021import marshal # Exposed through the API.
22import optparse
23import os # Somewhat exposed through the API.
24import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000025import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import re # Exposed through the API.
27import subprocess # Exposed through the API.
28import sys # Parts exposed through API.
29import tempfile # Exposed through the API.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000030import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000031import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000032import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000033import urllib2 # Exposed through the API.
maruel@chromium.org1e08c002009-05-28 19:09:33 +000034import warnings
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000035
36# Local imports.
37# TODO(joi) Would be cleaner to factor out utils in gcl to separate module, but
38# for now it would only be a couple of functions so hardly worth it.
39import gcl
maruel@chromium.org46a94102009-05-12 20:32:43 +000040import gclient
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000041import presubmit_canned_checks
42
43
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000044# Ask for feedback only once in program lifetime.
45_ASKED_FOR_FEEDBACK = False
46
47
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000048class NotImplementedException(Exception):
49 """We're leaving placeholders in a bunch of places to remind us of the
50 design of the API, but we have not implemented all of it yet. Implement as
51 the need arises.
52 """
53 pass
54
55
56def normpath(path):
57 '''Version of os.path.normpath that also changes backward slashes to
58 forward slashes when not running on Windows.
59 '''
60 # This is safe to always do because the Windows version of os.path.normpath
61 # will replace forward slashes with backward slashes.
62 path = path.replace(os.sep, '/')
63 return os.path.normpath(path)
64
65
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000066class OutputApi(object):
67 """This class (more like a module) gets passed to presubmit scripts so that
68 they can specify various types of results.
69 """
70
71 class PresubmitResult(object):
72 """Base class for result objects."""
73
74 def __init__(self, message, items=None, long_text=''):
75 """
76 message: A short one-line message to indicate errors.
77 items: A list of short strings to indicate where errors occurred.
78 long_text: multi-line text output, e.g. from another tool
79 """
80 self._message = message
81 self._items = []
82 if items:
83 self._items = items
84 self._long_text = long_text.rstrip()
85
86 def _Handle(self, output_stream, input_stream, may_prompt=True):
87 """Writes this result to the output stream.
88
89 Args:
90 output_stream: Where to write
91
92 Returns:
93 True if execution may continue, False otherwise.
94 """
95 output_stream.write(self._message)
96 output_stream.write('\n')
97 for item in self._items:
maruel@chromium.org5de13972009-06-10 18:16:06 +000098 output_stream.write(' %s\n' % str(item))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000099 if self._long_text:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000100 output_stream.write('\n***************\n%s\n***************\n' %
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000101 self._long_text)
102
103 if self.ShouldPrompt() and may_prompt:
104 output_stream.write('Are you sure you want to continue? (y/N): ')
105 response = input_stream.readline()
106 if response.strip().lower() != 'y':
107 return False
108
109 return not self.IsFatal()
110
111 def IsFatal(self):
112 """An error that is fatal stops g4 mail/submit immediately, i.e. before
113 other presubmit scripts are run.
114 """
115 return False
116
117 def ShouldPrompt(self):
118 """Whether this presubmit result should result in a prompt warning."""
119 return False
120
121 class PresubmitError(PresubmitResult):
122 """A hard presubmit error."""
123 def IsFatal(self):
124 return True
125
126 class PresubmitPromptWarning(PresubmitResult):
127 """An warning that prompts the user if they want to continue."""
128 def ShouldPrompt(self):
129 return True
130
131 class PresubmitNotifyResult(PresubmitResult):
132 """Just print something to the screen -- but it's not even a warning."""
133 pass
134
135 class MailTextResult(PresubmitResult):
136 """A warning that should be included in the review request email."""
137 def __init__(self, *args, **kwargs):
138 raise NotImplementedException() # TODO(joi) Implement.
139
140
141class InputApi(object):
142 """An instance of this object is passed to presubmit scripts so they can
143 know stuff about the change they're looking at.
144 """
145
maruel@chromium.org3410d912009-06-09 20:56:16 +0000146 # File extensions that are considered source files from a style guide
147 # perspective. Don't modify this list from a presubmit script!
148 DEFAULT_WHITE_LIST = (
149 # C++ and friends
150 r".*\.c", r".*\.cc", r".*\.cpp", r".*\.h", r".*\.m", r".*\.mm",
151 r".*\.inl", r".*\.asm", r".*\.hxx", r".*\.hpp",
152 # Scripts
153 r".*\.js", r".*\.py", r".*\.json", r".*\.sh", r".*\.rb",
154 # No extension at all
155 r"(^|.*[\\\/])[^.]+$",
156 # Other
157 r".*\.java", r".*\.mk", r".*\.am",
158 )
159
160 # Path regexp that should be excluded from being considered containing source
161 # files. Don't modify this list from a presubmit script!
162 DEFAULT_BLACK_LIST = (
163 r".*\bexperimental[\\\/].*",
164 r".*\bthird_party[\\\/].*",
165 # Output directories (just in case)
166 r".*\bDebug[\\\/].*",
167 r".*\bRelease[\\\/].*",
168 r".*\bxcodebuild[\\\/].*",
169 r".*\bsconsbuild[\\\/].*",
170 # All caps files like README and LICENCE.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000171 r".*\b[A-Z0-9_]+$",
172 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
173 r".*\.git[\\\/].*",
174 r".*\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000175 )
176
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000177 def __init__(self, change, presubmit_path, is_committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000178 """Builds an InputApi object.
179
180 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000181 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000182 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000183 is_committing: True if the change is about to be committed.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000184 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000185 # Version number of the presubmit_support script.
186 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000187 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000188 self.is_committing = is_committing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000189
190 # We expose various modules and functions as attributes of the input_api
191 # so that presubmit scripts don't have to import them.
192 self.basename = os.path.basename
193 self.cPickle = cPickle
194 self.cStringIO = cStringIO
195 self.os_path = os.path
196 self.pickle = pickle
197 self.marshal = marshal
198 self.re = re
199 self.subprocess = subprocess
200 self.tempfile = tempfile
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000201 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000202 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000203 self.urllib2 = urllib2
204
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000205 # To easily fork python.
206 self.python_executable = sys.executable
207 self.environ = os.environ
208
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000209 # InputApi.platform is the platform you're currently running on.
210 self.platform = sys.platform
211
212 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000213 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000214
215 # We carry the canned checks so presubmit scripts can easily use them.
216 self.canned_checks = presubmit_canned_checks
217
218 def PresubmitLocalPath(self):
219 """Returns the local path of the presubmit script currently being run.
220
221 This is useful if you don't want to hard-code absolute paths in the
222 presubmit script. For example, It can be used to find another file
223 relative to the PRESUBMIT.py script, so the whole tree can be branched and
224 the presubmit script still works, without editing its content.
225 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000226 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000227
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000228 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000229 """Translate a depot path to a local path (relative to client root).
230
231 Args:
232 Depot path as a string.
233
234 Returns:
235 The local path of the depot path under the user's current client, or None
236 if the file is not mapped.
237
238 Remember to check for the None case and show an appropriate error!
239 """
maruel@chromium.org46a94102009-05-12 20:32:43 +0000240 local_path = gclient.CaptureSVNInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000241 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000242 return local_path
243
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000244 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000245 """Translate a local path to a depot path.
246
247 Args:
248 Local path (relative to current directory, or absolute) as a string.
249
250 Returns:
251 The depot path (SVN URL) of the file if mapped, otherwise None.
252 """
maruel@chromium.org46a94102009-05-12 20:32:43 +0000253 depot_path = gclient.CaptureSVNInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000254 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000255 return depot_path
256
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000257 def AffectedFiles(self, include_dirs=False, include_deletes=True):
258 """Same as input_api.change.AffectedFiles() except only lists files
259 (and optionally directories) in the same directory as the current presubmit
260 script, or subdirectories thereof.
261 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000262 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000263 if len(dir_with_slash) == 1:
264 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000265 return filter(
266 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
267 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000268
269 def LocalPaths(self, include_dirs=False):
270 """Returns local paths of input_api.AffectedFiles()."""
271 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
272
273 def AbsoluteLocalPaths(self, include_dirs=False):
274 """Returns absolute local paths of input_api.AffectedFiles()."""
275 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
276
277 def ServerPaths(self, include_dirs=False):
278 """Returns server paths of input_api.AffectedFiles()."""
279 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
280
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000281 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000282 """Same as input_api.change.AffectedTextFiles() except only lists files
283 in the same directory as the current presubmit script, or subdirectories
284 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000285 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000286 if include_deletes is not None:
287 warnings.warn("AffectedTextFiles(include_deletes=%s)"
288 " is deprecated and ignored" % str(include_deletes),
289 category=DeprecationWarning,
290 stacklevel=2)
291 return filter(lambda x: x.IsTextFile(),
292 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000293
maruel@chromium.org3410d912009-06-09 20:56:16 +0000294 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
295 """Filters out files that aren't considered "source file".
296
297 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
298 and InputApi.DEFAULT_BLACK_LIST is used respectively.
299
300 The lists will be compiled as regular expression and
301 AffectedFile.LocalPath() needs to pass both list.
302
303 Note: Copy-paste this function to suit your needs or use a lambda function.
304 """
305 def Find(affected_file, list):
306 for item in list:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000307 local_path = affected_file.LocalPath()
308 if self.re.match(item, local_path):
309 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000310 return True
311 return False
312 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
313 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
314
315 def AffectedSourceFiles(self, source_file):
316 """Filter the list of AffectedTextFiles by the function source_file.
317
318 If source_file is None, InputApi.FilterSourceFile() is used.
319 """
320 if not source_file:
321 source_file = self.FilterSourceFile
322 return filter(source_file, self.AffectedTextFiles())
323
324 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000325 """An iterator over all text lines in "new" version of changed files.
326
327 Only lists lines from new or modified text files in the change that are
328 contained by the directory of the currently executing presubmit script.
329
330 This is useful for doing line-by-line regex checks, like checking for
331 trailing whitespace.
332
333 Yields:
334 a 3 tuple:
335 the AffectedFile instance of the current file;
336 integer line number (1-based); and
337 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000338
339 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000340 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000341 files = self.AffectedSourceFiles(source_file_filter)
342 return InputApi._RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000343
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000344 def ReadFile(self, file, mode='r'):
345 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000346
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000347 Deny reading anything outside the repository.
348 """
349 if isinstance(file, AffectedFile):
350 file = file.AbsoluteLocalPath()
351 if not file.startswith(self.change.RepositoryRoot()):
352 raise IOError('Access outside the repository root is denied.')
353 return gcl.ReadFile(file, mode)
354
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000355 @staticmethod
356 def _RightHandSideLinesImpl(affected_files):
357 """Implements RightHandSideLines for InputApi and GclChange."""
358 for af in affected_files:
359 lines = af.NewContents()
360 line_number = 0
361 for line in lines:
362 line_number += 1
363 yield (af, line_number, line)
364
365
366class AffectedFile(object):
367 """Representation of a file in a change."""
368
369 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000370 self._path = path
371 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000372 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000373 self._is_directory = None
374 self._properties = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000375
376 def ServerPath(self):
377 """Returns a path string that identifies the file in the SCM system.
378
379 Returns the empty string if the file does not exist in SCM.
380 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000381 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000382
383 def LocalPath(self):
384 """Returns the path of this file on the local disk relative to client root.
385 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000386 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000387
388 def AbsoluteLocalPath(self):
389 """Returns the absolute path of this file on the local disk.
390 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000391 return normpath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000392
393 def IsDirectory(self):
394 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000395 if self._is_directory is None:
396 path = self.AbsoluteLocalPath()
397 self._is_directory = (os.path.exists(path) and
398 os.path.isdir(path))
399 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000400
401 def Action(self):
402 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000403 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
404 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000405 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000406
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000407 def Property(self, property_name):
408 """Returns the specified SCM property of this file, or None if no such
409 property.
410 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000411 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000412
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000413 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000414 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000415
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000416 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000417 raise NotImplementedError() # Implement when needed
418
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000419 def NewContents(self):
420 """Returns an iterator over the lines in the new version of file.
421
422 The new version is the file in the user's workspace, i.e. the "right hand
423 side".
424
425 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000426 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000427 """
428 if self.IsDirectory():
429 return []
430 else:
431 return gcl.ReadFile(self.AbsoluteLocalPath()).splitlines()
432
433 def OldContents(self):
434 """Returns an iterator over the lines in the old version of file.
435
436 The old version is the file in depot, i.e. the "left hand side".
437 """
438 raise NotImplementedError() # Implement when needed
439
440 def OldFileTempPath(self):
441 """Returns the path on local disk where the old contents resides.
442
443 The old version is the file in depot, i.e. the "left hand side".
444 This is a read-only cached copy of the old contents. *DO NOT* try to
445 modify this file.
446 """
447 raise NotImplementedError() # Implement if/when needed.
448
maruel@chromium.org5de13972009-06-10 18:16:06 +0000449 def __str__(self):
450 return self.LocalPath()
451
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000452
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000453class SvnAffectedFile(AffectedFile):
454 """Representation of a file in a change out of a Subversion checkout."""
455
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000456 def __init__(self, *args, **kwargs):
457 AffectedFile.__init__(self, *args, **kwargs)
458 self._server_path = None
459 self._is_text_file = None
460
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000461 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000462 if self._server_path is None:
463 self._server_path = gclient.CaptureSVNInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000464 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000465 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000466
467 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000468 if self._is_directory is None:
469 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000470 if os.path.exists(path):
471 # Retrieve directly from the file system; it is much faster than
472 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000473 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000474 else:
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000475 self._is_directory = gclient.CaptureSVNInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000476 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000477 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000478
479 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000480 if not property_name in self._properties:
481 self._properties[property_name] = gcl.GetSVNFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000482 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000483 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000484
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000485 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000486 if self._is_text_file is None:
487 if self.Action() == 'D':
488 # A deleted file is not a text file.
489 self._is_text_file = False
490 elif self.IsDirectory():
491 self._is_text_file = False
492 else:
493 mime_type = gcl.GetSVNFileProperty(self.AbsoluteLocalPath(),
494 'svn:mime-type')
495 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
496 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000497
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000498
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000499class GitAffectedFile(AffectedFile):
500 """Representation of a file in a change out of a git checkout."""
501
502 def __init__(self, *args, **kwargs):
503 AffectedFile.__init__(self, *args, **kwargs)
504 self._server_path = None
505 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000506
507 def ServerPath(self):
508 if self._server_path is None:
509 raise NotImplementedException() # TODO(maruel) Implement.
510 return self._server_path
511
512 def IsDirectory(self):
513 if self._is_directory is None:
514 path = self.AbsoluteLocalPath()
515 if os.path.exists(path):
516 # Retrieve directly from the file system; it is much faster than
517 # querying subversion, especially on Windows.
518 self._is_directory = os.path.isdir(path)
519 else:
520 # raise NotImplementedException() # TODO(maruel) Implement.
521 self._is_directory = False
522 return self._is_directory
523
524 def Property(self, property_name):
525 if not property_name in self._properties:
526 raise NotImplementedException() # TODO(maruel) Implement.
527 return self._properties[property_name]
528
529 def IsTextFile(self):
530 if self._is_text_file is None:
531 if self.Action() == 'D':
532 # A deleted file is not a text file.
533 self._is_text_file = False
534 elif self.IsDirectory():
535 self._is_text_file = False
536 else:
537 # raise NotImplementedException() # TODO(maruel) Implement.
538 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
539 return self._is_text_file
540
541
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000542class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000543 """Describe a change.
544
545 Used directly by the presubmit scripts to query the current change being
546 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000547
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000548 Instance members:
549 tags: Dictionnary of KEY=VALUE pairs found in the change description.
550 self.KEY: equivalent to tags['KEY']
551 """
552
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000553 _AFFECTED_FILES = AffectedFile
554
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000555 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000556 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000557 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000558
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000559 def __init__(self, name, description, local_root, files, issue, patchset):
560 if files is None:
561 files = []
562 self._name = name
563 self._full_description = description
564 self._local_root = local_root
565 self.issue = issue
566 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000567 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000568
569 # From the description text, build up a dictionary of key/value pairs
570 # plus the description minus all key/value or "tag" lines.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000571 self._description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000572 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000573 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000574 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000575 if m:
576 self.tags[m.group('key')] = m.group('value')
577 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000578 self._description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000579
580 # Change back to text and remove whitespace at end.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000581 self._description_without_tags = '\n'.join(self._description_without_tags)
582 self._description_without_tags = self._description_without_tags.rstrip()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000583
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000584 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000585 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
586 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000587 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000588
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000589 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000590 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000591 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000592
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000593 def DescriptionText(self):
594 """Returns the user-entered changelist description, minus tags.
595
596 Any line in the user-provided description starting with e.g. "FOO="
597 (whitespace permitted before and around) is considered a tag line. Such
598 lines are stripped out of the description this function returns.
599 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000600 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000601
602 def FullDescriptionText(self):
603 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000604 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000605
606 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000607 """Returns the repository (checkout) root directory for this change,
608 as an absolute path.
609 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000610 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000611
612 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000613 """Return tags directly as attributes on the object."""
614 if not re.match(r"^[A-Z_]*$", attr):
615 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000616 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000617
618 def AffectedFiles(self, include_dirs=False, include_deletes=True):
619 """Returns a list of AffectedFile instances for all files in the change.
620
621 Args:
622 include_deletes: If false, deleted files will be filtered out.
623 include_dirs: True to include directories in the list
624
625 Returns:
626 [AffectedFile(path, action), AffectedFile(path, action)]
627 """
628 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000629 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000630 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000631 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000632
633 if include_deletes:
634 return affected
635 else:
636 return filter(lambda x: x.Action() != 'D', affected)
637
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000638 def AffectedTextFiles(self, include_deletes=None):
639 """Return a list of the existing text files in a change."""
640 if include_deletes is not None:
641 warnings.warn("AffectedTextFiles(include_deletes=%s)"
642 " is deprecated and ignored" % str(include_deletes),
643 category=DeprecationWarning,
644 stacklevel=2)
645 return filter(lambda x: x.IsTextFile(),
646 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000647
648 def LocalPaths(self, include_dirs=False):
649 """Convenience function."""
650 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
651
652 def AbsoluteLocalPaths(self, include_dirs=False):
653 """Convenience function."""
654 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
655
656 def ServerPaths(self, include_dirs=False):
657 """Convenience function."""
658 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
659
660 def RightHandSideLines(self):
661 """An iterator over all text lines in "new" version of changed files.
662
663 Lists lines from new or modified text files in the change.
664
665 This is useful for doing line-by-line regex checks, like checking for
666 trailing whitespace.
667
668 Yields:
669 a 3 tuple:
670 the AffectedFile instance of the current file;
671 integer line number (1-based); and
672 the contents of the line as a string.
673 """
674 return InputApi._RightHandSideLinesImpl(
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000675 filter(lambda x: x.IsTextFile(),
676 self.AffectedFiles(include_deletes=False)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000677
678
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000679class SvnChange(Change):
680 _AFFECTED_FILES = SvnAffectedFile
681
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000682 def __init__(self, *args, **kwargs):
683 Change.__init__(self, *args, **kwargs)
684 self.scm = 'svn'
685
686 def GetAllModifiedFiles(self):
687 """Get all modified files."""
688 changelists = gcl.GetModifiedFiles()
689 all_modified_files = []
690 for cl in changelists.values():
691 all_modified_files.extend([f[1] for f in cl])
692 return all_modified_files
693
694 def GetModifiedFiles(self):
695 """Get modified files in the current CL."""
696 changelists = gcl.GetModifiedFiles()
697 return [f[1] for f in changelists[self.Name()]]
698
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000699
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000700class GitChange(Change):
701 _AFFECTED_FILES = GitAffectedFile
702
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000703 def __init__(self, *args, **kwargs):
704 Change.__init__(self, *args, **kwargs)
705 self.scm = 'git'
706
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000707
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000708def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709 """Finds all presubmit files that apply to a given set of source files.
710
711 Args:
712 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000713 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000714
715 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000716 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717 """
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000718 entries = []
719 for f in files:
720 f = normpath(os.path.join(root, f))
721 while f:
722 f = os.path.dirname(f)
723 if f in entries:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000724 break
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000725 entries.append(f)
726 if f == root:
727 break
728 entries.sort()
729 entries = map(lambda x: os.path.join(x, 'PRESUBMIT.py'), entries)
730 return filter(lambda x: os.path.isfile(x), entries)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000731
732
733class PresubmitExecuter(object):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000734 def __init__(self, change, committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000735 """
736 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000737 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000738 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
739 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000740 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000741 self.committing = committing
742
743 def ExecPresubmitScript(self, script_text, presubmit_path):
744 """Executes a single presubmit script.
745
746 Args:
747 script_text: The text of the presubmit script.
748 presubmit_path: The path to the presubmit file (this will be reported via
749 input_api.PresubmitLocalPath()).
750
751 Return:
752 A list of result objects, empty if no problems.
753 """
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000754 input_api = InputApi(self.change, presubmit_path, self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000755 context = {}
756 exec script_text in context
757
758 # These function names must change if we make substantial changes to
759 # the presubmit API that are not backwards compatible.
760 if self.committing:
761 function_name = 'CheckChangeOnCommit'
762 else:
763 function_name = 'CheckChangeOnUpload'
764 if function_name in context:
765 context['__args'] = (input_api, OutputApi())
766 result = eval(function_name + '(*__args)', context)
767 if not (isinstance(result, types.TupleType) or
768 isinstance(result, types.ListType)):
769 raise exceptions.RuntimeError(
770 'Presubmit functions must return a tuple or list')
771 for item in result:
772 if not isinstance(item, OutputApi.PresubmitResult):
773 raise exceptions.RuntimeError(
774 'All presubmit results must be of types derived from '
775 'output_api.PresubmitResult')
776 else:
777 result = () # no error since the script doesn't care about current event.
778
779 return result
780
781
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000782def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000783 committing,
784 verbose,
785 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000786 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000787 default_presubmit,
788 may_prompt):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000789 """Runs all presubmit checks that apply to the files in the change.
790
791 This finds all PRESUBMIT.py files in directories enclosing the files in the
792 change (up to the repository root) and calls the relevant entrypoint function
793 depending on whether the change is being committed or uploaded.
794
795 Prints errors, warnings and notifications. Prompts the user for warnings
796 when needed.
797
798 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000799 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000800 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
801 verbose: Prints debug info.
802 output_stream: A stream to write output from presubmit tests to.
803 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000804 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000805 may_prompt: Enable (y/n) questions on warning or error.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000806
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000807 Warning:
808 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
809 SHOULD be sys.stdin.
810
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000811 Return:
812 True if execution can continue, False if not.
813 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000814 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
815 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000816 if not presubmit_files and verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000817 output_stream.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000818 results = []
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000819 executer = PresubmitExecuter(change, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000820 if default_presubmit:
821 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000822 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000823 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000824 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000825 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +0000826 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000827 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000828 output_stream.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +0000829 # Accept CRLF presubmit script.
maruel@chromium.org277003e2009-05-01 12:51:43 +0000830 presubmit_script = gcl.ReadFile(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000831 results += executer.ExecPresubmitScript(presubmit_script, filename)
832
833 errors = []
834 notifications = []
835 warnings = []
836 for result in results:
837 if not result.IsFatal() and not result.ShouldPrompt():
838 notifications.append(result)
839 elif result.ShouldPrompt():
840 warnings.append(result)
841 else:
842 errors.append(result)
843
844 error_count = 0
845 for name, items in (('Messages', notifications),
846 ('Warnings', warnings),
847 ('ERRORS', errors)):
848 if items:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000849 output_stream.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000850 for item in items:
851 if not item._Handle(output_stream, input_stream,
852 may_prompt=False):
853 error_count += 1
854 output_stream.write('\n')
maruel@chromium.org07bbc212009-06-11 02:08:41 +0000855 if not errors and warnings and may_prompt:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000856 output_stream.write(
857 'There were presubmit warnings. Sure you want to continue? (y/N): ')
858 response = input_stream.readline()
859 if response.strip().lower() != 'y':
860 error_count += 1
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000861
862 global _ASKED_FOR_FEEDBACK
863 # Ask for feedback one time out of 5.
864 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
865 output_stream.write("Was the presubmit check useful? Please send feedback "
866 "& hate mail to maruel@chromium.org!\n")
867 _ASKED_FOR_FEEDBACK = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000868 return (error_count == 0)
869
870
871def ScanSubDirs(mask, recursive):
872 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000873 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 +0000874 else:
875 results = []
876 for root, dirs, files in os.walk('.'):
877 if '.svn' in dirs:
878 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000879 if '.git' in dirs:
880 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000881 for name in files:
882 if fnmatch.fnmatch(name, mask):
883 results.append(os.path.join(root, name))
884 return results
885
886
887def ParseFiles(args, recursive):
888 files = []
889 for arg in args:
890 files.extend([('M', file) for file in ScanSubDirs(arg, recursive)])
891 return files
892
893
894def Main(argv):
895 parser = optparse.OptionParser(usage="%prog [options]",
896 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000897 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000898 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000899 parser.add_option("-u", "--upload", action="store_false", dest='commit',
900 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000901 parser.add_option("-r", "--recursive", action="store_true",
902 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000903 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000904 help="Verbose output")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000905 parser.add_option("--files")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000906 parser.add_option("--name", default='no name')
907 parser.add_option("--description", default='')
908 parser.add_option("--issue", type='int', default=0)
909 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000910 parser.add_option("--root", default='')
911 parser.add_option("--default_presubmit")
912 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000913 options, args = parser.parse_args(argv[1:])
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000914 if not options.root:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000915 options.root = os.getcwd()
916 if os.path.isdir(os.path.join(options.root, '.git')):
917 change_class = GitChange
918 if not options.files:
919 if args:
920 options.files = ParseFiles(args, options.recursive)
921 else:
922 # Grab modified files.
923 raise NotImplementedException() # TODO(maruel) Implement.
924 elif os.path.isdir(os.path.join(options.root, '.svn')):
925 change_class = SvnChange
926 if not options.files:
927 if args:
928 options.files = ParseFiles(args, options.recursive)
929 else:
930 # Grab modified files.
931 files = gclient.CaptureSVNStatus([options.root])
932 else:
933 # Doesn't seem under source control.
934 change_class = Change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000935 if options.verbose:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000936 print "Found %d files." % len(options.files)
937 return not DoPresubmitChecks(change_class(options.name,
938 options.description,
939 options.root,
940 options.files,
941 options.issue,
942 options.patchset),
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000943 options.commit,
944 options.verbose,
945 sys.stdout,
946 sys.stdin,
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000947 options.default_presubmit,
948 options.may_prompt)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000949
950
951if __name__ == '__main__':
952 sys.exit(Main(sys.argv))