blob: b4ee311972b8ff2395345fc7dc7356f647fc8652 [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
thestig@chromium.orgde243452009-10-06 21:02:56 +00009__version__ = '1.3.3'
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.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000030import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000031import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000033import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034import urllib2 # Exposed through the API.
maruel@chromium.org1e08c002009-05-28 19:09:33 +000035import warnings
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000036
37# Local imports.
38# TODO(joi) Would be cleaner to factor out utils in gcl to separate module, but
39# for now it would only be a couple of functions so hardly worth it.
40import gcl
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000041import gclient_scm
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000042import presubmit_canned_checks
43
44
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000045# Ask for feedback only once in program lifetime.
46_ASKED_FOR_FEEDBACK = False
47
48
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000049class NotImplementedException(Exception):
50 """We're leaving placeholders in a bunch of places to remind us of the
51 design of the API, but we have not implemented all of it yet. Implement as
52 the need arises.
53 """
54 pass
55
56
57def normpath(path):
58 '''Version of os.path.normpath that also changes backward slashes to
59 forward slashes when not running on Windows.
60 '''
61 # This is safe to always do because the Windows version of os.path.normpath
62 # will replace forward slashes with backward slashes.
63 path = path.replace(os.sep, '/')
64 return os.path.normpath(path)
65
66
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000067class OutputApi(object):
68 """This class (more like a module) gets passed to presubmit scripts so that
69 they can specify various types of results.
70 """
71
72 class PresubmitResult(object):
73 """Base class for result objects."""
74
75 def __init__(self, message, items=None, long_text=''):
76 """
77 message: A short one-line message to indicate errors.
78 items: A list of short strings to indicate where errors occurred.
79 long_text: multi-line text output, e.g. from another tool
80 """
81 self._message = message
82 self._items = []
83 if items:
84 self._items = items
85 self._long_text = long_text.rstrip()
86
87 def _Handle(self, output_stream, input_stream, may_prompt=True):
88 """Writes this result to the output stream.
89
90 Args:
91 output_stream: Where to write
92
93 Returns:
94 True if execution may continue, False otherwise.
95 """
96 output_stream.write(self._message)
97 output_stream.write('\n')
98 for item in self._items:
maruel@chromium.org5de13972009-06-10 18:16:06 +000099 output_stream.write(' %s\n' % str(item))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000100 if self._long_text:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000101 output_stream.write('\n***************\n%s\n***************\n' %
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000102 self._long_text)
103
104 if self.ShouldPrompt() and may_prompt:
105 output_stream.write('Are you sure you want to continue? (y/N): ')
106 response = input_stream.readline()
107 if response.strip().lower() != 'y':
108 return False
109
110 return not self.IsFatal()
111
112 def IsFatal(self):
113 """An error that is fatal stops g4 mail/submit immediately, i.e. before
114 other presubmit scripts are run.
115 """
116 return False
117
118 def ShouldPrompt(self):
119 """Whether this presubmit result should result in a prompt warning."""
120 return False
121
122 class PresubmitError(PresubmitResult):
123 """A hard presubmit error."""
124 def IsFatal(self):
125 return True
126
127 class PresubmitPromptWarning(PresubmitResult):
128 """An warning that prompts the user if they want to continue."""
129 def ShouldPrompt(self):
130 return True
131
132 class PresubmitNotifyResult(PresubmitResult):
133 """Just print something to the screen -- but it's not even a warning."""
134 pass
135
136 class MailTextResult(PresubmitResult):
137 """A warning that should be included in the review request email."""
138 def __init__(self, *args, **kwargs):
139 raise NotImplementedException() # TODO(joi) Implement.
140
141
142class InputApi(object):
143 """An instance of this object is passed to presubmit scripts so they can
144 know stuff about the change they're looking at.
145 """
146
maruel@chromium.org3410d912009-06-09 20:56:16 +0000147 # File extensions that are considered source files from a style guide
148 # perspective. Don't modify this list from a presubmit script!
149 DEFAULT_WHITE_LIST = (
150 # C++ and friends
151 r".*\.c", r".*\.cc", r".*\.cpp", r".*\.h", r".*\.m", r".*\.mm",
152 r".*\.inl", r".*\.asm", r".*\.hxx", r".*\.hpp",
153 # Scripts
154 r".*\.js", r".*\.py", r".*\.json", r".*\.sh", r".*\.rb",
155 # No extension at all
156 r"(^|.*[\\\/])[^.]+$",
157 # Other
maruel@chromium.orgd59982a2009-08-24 15:48:47 +0000158 r".*\.java", r".*\.mk", r".*\.am", r".*\.txt",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000159 )
160
161 # Path regexp that should be excluded from being considered containing source
162 # files. Don't modify this list from a presubmit script!
163 DEFAULT_BLACK_LIST = (
164 r".*\bexperimental[\\\/].*",
165 r".*\bthird_party[\\\/].*",
166 # Output directories (just in case)
167 r".*\bDebug[\\\/].*",
168 r".*\bRelease[\\\/].*",
169 r".*\bxcodebuild[\\\/].*",
170 r".*\bsconsbuild[\\\/].*",
171 # All caps files like README and LICENCE.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000172 r".*\b[A-Z0-9_]+$",
173 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
174 r".*\.git[\\\/].*",
175 r".*\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000176 )
177
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000178 def __init__(self, change, presubmit_path, is_committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000179 """Builds an InputApi object.
180
181 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000182 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000183 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000184 is_committing: True if the change is about to be committed.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000185 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000186 # Version number of the presubmit_support script.
187 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000188 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000189 self.is_committing = is_committing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000190
191 # We expose various modules and functions as attributes of the input_api
192 # so that presubmit scripts don't have to import them.
193 self.basename = os.path.basename
194 self.cPickle = cPickle
195 self.cStringIO = cStringIO
196 self.os_path = os.path
197 self.pickle = pickle
198 self.marshal = marshal
199 self.re = re
200 self.subprocess = subprocess
201 self.tempfile = tempfile
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000202 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000203 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000204 self.urllib2 = urllib2
205
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000206 # To easily fork python.
207 self.python_executable = sys.executable
208 self.environ = os.environ
209
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000210 # InputApi.platform is the platform you're currently running on.
211 self.platform = sys.platform
212
213 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000214 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000215
216 # We carry the canned checks so presubmit scripts can easily use them.
217 self.canned_checks = presubmit_canned_checks
218
219 def PresubmitLocalPath(self):
220 """Returns the local path of the presubmit script currently being run.
221
222 This is useful if you don't want to hard-code absolute paths in the
223 presubmit script. For example, It can be used to find another file
224 relative to the PRESUBMIT.py script, so the whole tree can be branched and
225 the presubmit script still works, without editing its content.
226 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000227 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000228
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000229 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000230 """Translate a depot path to a local path (relative to client root).
231
232 Args:
233 Depot path as a string.
234
235 Returns:
236 The local path of the depot path under the user's current client, or None
237 if the file is not mapped.
238
239 Remember to check for the None case and show an appropriate error!
240 """
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000241 local_path = gclient_scm.CaptureSVNInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000242 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000243 return local_path
244
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000245 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000246 """Translate a local path to a depot path.
247
248 Args:
249 Local path (relative to current directory, or absolute) as a string.
250
251 Returns:
252 The depot path (SVN URL) of the file if mapped, otherwise None.
253 """
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000254 depot_path = gclient_scm.CaptureSVNInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000255 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256 return depot_path
257
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000258 def AffectedFiles(self, include_dirs=False, include_deletes=True):
259 """Same as input_api.change.AffectedFiles() except only lists files
260 (and optionally directories) in the same directory as the current presubmit
261 script, or subdirectories thereof.
262 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000263 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000264 if len(dir_with_slash) == 1:
265 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000266 return filter(
267 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
268 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000269
270 def LocalPaths(self, include_dirs=False):
271 """Returns local paths of input_api.AffectedFiles()."""
272 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
273
274 def AbsoluteLocalPaths(self, include_dirs=False):
275 """Returns absolute local paths of input_api.AffectedFiles()."""
276 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
277
278 def ServerPaths(self, include_dirs=False):
279 """Returns server paths of input_api.AffectedFiles()."""
280 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
281
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000282 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000283 """Same as input_api.change.AffectedTextFiles() except only lists files
284 in the same directory as the current presubmit script, or subdirectories
285 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000286 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000287 if include_deletes is not None:
288 warnings.warn("AffectedTextFiles(include_deletes=%s)"
289 " is deprecated and ignored" % str(include_deletes),
290 category=DeprecationWarning,
291 stacklevel=2)
292 return filter(lambda x: x.IsTextFile(),
293 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000294
maruel@chromium.org3410d912009-06-09 20:56:16 +0000295 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
296 """Filters out files that aren't considered "source file".
297
298 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
299 and InputApi.DEFAULT_BLACK_LIST is used respectively.
300
301 The lists will be compiled as regular expression and
302 AffectedFile.LocalPath() needs to pass both list.
303
304 Note: Copy-paste this function to suit your needs or use a lambda function.
305 """
306 def Find(affected_file, list):
307 for item in list:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000308 local_path = affected_file.LocalPath()
309 if self.re.match(item, local_path):
310 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000311 return True
312 return False
313 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
314 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
315
316 def AffectedSourceFiles(self, source_file):
317 """Filter the list of AffectedTextFiles by the function source_file.
318
319 If source_file is None, InputApi.FilterSourceFile() is used.
320 """
321 if not source_file:
322 source_file = self.FilterSourceFile
323 return filter(source_file, self.AffectedTextFiles())
324
325 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000326 """An iterator over all text lines in "new" version of changed files.
327
328 Only lists lines from new or modified text files in the change that are
329 contained by the directory of the currently executing presubmit script.
330
331 This is useful for doing line-by-line regex checks, like checking for
332 trailing whitespace.
333
334 Yields:
335 a 3 tuple:
336 the AffectedFile instance of the current file;
337 integer line number (1-based); and
338 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000339
340 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000341 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000342 files = self.AffectedSourceFiles(source_file_filter)
343 return InputApi._RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000344
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000345 def ReadFile(self, file, mode='r'):
346 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000347
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000348 Deny reading anything outside the repository.
349 """
350 if isinstance(file, AffectedFile):
351 file = file.AbsoluteLocalPath()
352 if not file.startswith(self.change.RepositoryRoot()):
353 raise IOError('Access outside the repository root is denied.')
354 return gcl.ReadFile(file, mode)
355
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000356 @staticmethod
357 def _RightHandSideLinesImpl(affected_files):
358 """Implements RightHandSideLines for InputApi and GclChange."""
359 for af in affected_files:
360 lines = af.NewContents()
361 line_number = 0
362 for line in lines:
363 line_number += 1
364 yield (af, line_number, line)
365
366
367class AffectedFile(object):
368 """Representation of a file in a change."""
369
370 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000371 self._path = path
372 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000373 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000374 self._is_directory = None
375 self._properties = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000376
377 def ServerPath(self):
378 """Returns a path string that identifies the file in the SCM system.
379
380 Returns the empty string if the file does not exist in SCM.
381 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000382 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000383
384 def LocalPath(self):
385 """Returns the path of this file on the local disk relative to client root.
386 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000387 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000388
389 def AbsoluteLocalPath(self):
390 """Returns the absolute path of this file on the local disk.
391 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000392 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000393
394 def IsDirectory(self):
395 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000396 if self._is_directory is None:
397 path = self.AbsoluteLocalPath()
398 self._is_directory = (os.path.exists(path) and
399 os.path.isdir(path))
400 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000401
402 def Action(self):
403 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000404 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
405 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000406 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000407
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000408 def Property(self, property_name):
409 """Returns the specified SCM property of this file, or None if no such
410 property.
411 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000412 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000413
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000414 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000415 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000416
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000417 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000418 raise NotImplementedError() # Implement when needed
419
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000420 def NewContents(self):
421 """Returns an iterator over the lines in the new version of file.
422
423 The new version is the file in the user's workspace, i.e. the "right hand
424 side".
425
426 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000427 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000428 """
429 if self.IsDirectory():
430 return []
431 else:
432 return gcl.ReadFile(self.AbsoluteLocalPath()).splitlines()
433
434 def OldContents(self):
435 """Returns an iterator over the lines in the old version of file.
436
437 The old version is the file in depot, i.e. the "left hand side".
438 """
439 raise NotImplementedError() # Implement when needed
440
441 def OldFileTempPath(self):
442 """Returns the path on local disk where the old contents resides.
443
444 The old version is the file in depot, i.e. the "left hand side".
445 This is a read-only cached copy of the old contents. *DO NOT* try to
446 modify this file.
447 """
448 raise NotImplementedError() # Implement if/when needed.
449
maruel@chromium.org5de13972009-06-10 18:16:06 +0000450 def __str__(self):
451 return self.LocalPath()
452
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000453
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000454class SvnAffectedFile(AffectedFile):
455 """Representation of a file in a change out of a Subversion checkout."""
456
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000457 def __init__(self, *args, **kwargs):
458 AffectedFile.__init__(self, *args, **kwargs)
459 self._server_path = None
460 self._is_text_file = None
461
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000462 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000463 if self._server_path is None:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000464 self._server_path = gclient_scm.CaptureSVNInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000465 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000466 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000467
468 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000469 if self._is_directory is None:
470 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000471 if os.path.exists(path):
472 # Retrieve directly from the file system; it is much faster than
473 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000474 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000475 else:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000476 self._is_directory = gclient_scm.CaptureSVNInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000477 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000478 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000479
480 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000481 if not property_name in self._properties:
482 self._properties[property_name] = gcl.GetSVNFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000483 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000484 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000485
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000486 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000487 if self._is_text_file is None:
488 if self.Action() == 'D':
489 # A deleted file is not a text file.
490 self._is_text_file = False
491 elif self.IsDirectory():
492 self._is_text_file = False
493 else:
494 mime_type = gcl.GetSVNFileProperty(self.AbsoluteLocalPath(),
495 'svn:mime-type')
496 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
497 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000498
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000499
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000500class GitAffectedFile(AffectedFile):
501 """Representation of a file in a change out of a git checkout."""
502
503 def __init__(self, *args, **kwargs):
504 AffectedFile.__init__(self, *args, **kwargs)
505 self._server_path = None
506 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000507
508 def ServerPath(self):
509 if self._server_path is None:
510 raise NotImplementedException() # TODO(maruel) Implement.
511 return self._server_path
512
513 def IsDirectory(self):
514 if self._is_directory is None:
515 path = self.AbsoluteLocalPath()
516 if os.path.exists(path):
517 # Retrieve directly from the file system; it is much faster than
518 # querying subversion, especially on Windows.
519 self._is_directory = os.path.isdir(path)
520 else:
521 # raise NotImplementedException() # TODO(maruel) Implement.
522 self._is_directory = False
523 return self._is_directory
524
525 def Property(self, property_name):
526 if not property_name in self._properties:
527 raise NotImplementedException() # TODO(maruel) Implement.
528 return self._properties[property_name]
529
530 def IsTextFile(self):
531 if self._is_text_file is None:
532 if self.Action() == 'D':
533 # A deleted file is not a text file.
534 self._is_text_file = False
535 elif self.IsDirectory():
536 self._is_text_file = False
537 else:
538 # raise NotImplementedException() # TODO(maruel) Implement.
539 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
540 return self._is_text_file
541
542
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000543class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000544 """Describe a change.
545
546 Used directly by the presubmit scripts to query the current change being
547 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000548
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000549 Instance members:
550 tags: Dictionnary of KEY=VALUE pairs found in the change description.
551 self.KEY: equivalent to tags['KEY']
552 """
553
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000554 _AFFECTED_FILES = AffectedFile
555
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000556 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000557 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000558 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000559
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000560 def __init__(self, name, description, local_root, files, issue, patchset):
561 if files is None:
562 files = []
563 self._name = name
564 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000565 # Convert root into an absolute path.
566 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000567 self.issue = issue
568 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000569 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000570
571 # From the description text, build up a dictionary of key/value pairs
572 # plus the description minus all key/value or "tag" lines.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000573 self._description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000574 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000575 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000576 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000577 if m:
578 self.tags[m.group('key')] = m.group('value')
579 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000580 self._description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000581
582 # Change back to text and remove whitespace at end.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000583 self._description_without_tags = '\n'.join(self._description_without_tags)
584 self._description_without_tags = self._description_without_tags.rstrip()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000585
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000586 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000587 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
588 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000589 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000590
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000591 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000592 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000593 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000594
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000595 def DescriptionText(self):
596 """Returns the user-entered changelist description, minus tags.
597
598 Any line in the user-provided description starting with e.g. "FOO="
599 (whitespace permitted before and around) is considered a tag line. Such
600 lines are stripped out of the description this function returns.
601 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000602 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000603
604 def FullDescriptionText(self):
605 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000606 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000607
608 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000609 """Returns the repository (checkout) root directory for this change,
610 as an absolute path.
611 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000612 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000613
614 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000615 """Return tags directly as attributes on the object."""
616 if not re.match(r"^[A-Z_]*$", attr):
617 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000618 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000619
620 def AffectedFiles(self, include_dirs=False, include_deletes=True):
621 """Returns a list of AffectedFile instances for all files in the change.
622
623 Args:
624 include_deletes: If false, deleted files will be filtered out.
625 include_dirs: True to include directories in the list
626
627 Returns:
628 [AffectedFile(path, action), AffectedFile(path, action)]
629 """
630 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000631 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000632 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000633 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000634
635 if include_deletes:
636 return affected
637 else:
638 return filter(lambda x: x.Action() != 'D', affected)
639
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000640 def AffectedTextFiles(self, include_deletes=None):
641 """Return a list of the existing text files in a change."""
642 if include_deletes is not None:
643 warnings.warn("AffectedTextFiles(include_deletes=%s)"
644 " is deprecated and ignored" % str(include_deletes),
645 category=DeprecationWarning,
646 stacklevel=2)
647 return filter(lambda x: x.IsTextFile(),
648 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000649
650 def LocalPaths(self, include_dirs=False):
651 """Convenience function."""
652 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
653
654 def AbsoluteLocalPaths(self, include_dirs=False):
655 """Convenience function."""
656 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
657
658 def ServerPaths(self, include_dirs=False):
659 """Convenience function."""
660 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
661
662 def RightHandSideLines(self):
663 """An iterator over all text lines in "new" version of changed files.
664
665 Lists lines from new or modified text files in the change.
666
667 This is useful for doing line-by-line regex checks, like checking for
668 trailing whitespace.
669
670 Yields:
671 a 3 tuple:
672 the AffectedFile instance of the current file;
673 integer line number (1-based); and
674 the contents of the line as a string.
675 """
676 return InputApi._RightHandSideLinesImpl(
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000677 filter(lambda x: x.IsTextFile(),
678 self.AffectedFiles(include_deletes=False)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000679
680
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000681class SvnChange(Change):
682 _AFFECTED_FILES = SvnAffectedFile
683
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000684 def __init__(self, *args, **kwargs):
685 Change.__init__(self, *args, **kwargs)
686 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000687 self._changelists = None
688
689 def _GetChangeLists(self):
690 """Get all change lists."""
691 if self._changelists == None:
692 previous_cwd = os.getcwd()
693 os.chdir(self.RepositoryRoot())
694 self._changelists = gcl.GetModifiedFiles()
695 os.chdir(previous_cwd)
696 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000697
698 def GetAllModifiedFiles(self):
699 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000700 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000701 all_modified_files = []
702 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000703 all_modified_files.extend(
704 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000705 return all_modified_files
706
707 def GetModifiedFiles(self):
708 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000709 changelists = self._GetChangeLists()
710 return [os.path.join(self.RepositoryRoot(), f[1])
711 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000712
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000713
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000714class GitChange(Change):
715 _AFFECTED_FILES = GitAffectedFile
716
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000717 def __init__(self, *args, **kwargs):
718 Change.__init__(self, *args, **kwargs)
719 self.scm = 'git'
720
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000721
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000722def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000723 """Finds all presubmit files that apply to a given set of source files.
724
725 Args:
726 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000727 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000728
729 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000730 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000731 """
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000732 entries = []
733 for f in files:
734 f = normpath(os.path.join(root, f))
735 while f:
736 f = os.path.dirname(f)
737 if f in entries:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000738 break
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000739 entries.append(f)
740 if f == root:
741 break
742 entries.sort()
743 entries = map(lambda x: os.path.join(x, 'PRESUBMIT.py'), entries)
744 return filter(lambda x: os.path.isfile(x), entries)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000745
746
thestig@chromium.orgde243452009-10-06 21:02:56 +0000747class GetTrySlavesExecuter(object):
748 def ExecPresubmitScript(self, script_text):
749 """Executes GetPreferredTrySlaves() from a single presubmit script.
750
751 Args:
752 script_text: The text of the presubmit script.
753
754 Return:
755 A list of try slaves.
756 """
757 context = {}
758 exec script_text in context
759
760 function_name = 'GetPreferredTrySlaves'
761 if function_name in context:
762 result = eval(function_name + '()', context)
763 if not isinstance(result, types.ListType):
764 raise exceptions.RuntimeError(
765 'Presubmit functions must return a list, got a %s instead: %s' %
766 (type(result), str(result)))
767 for item in result:
768 if not isinstance(item, basestring):
769 raise exceptions.RuntimeError('All try slaves names must be strings.')
770 if item != item.strip():
771 raise exceptions.RuntimeError('Try slave names cannot start/end'
772 'with whitespace')
773 else:
774 result = []
775 return result
776
777
778def DoGetTrySlaves(changed_files,
779 repository_root,
780 default_presubmit,
781 verbose,
782 output_stream):
783 """Get the list of try servers from the presubmit scripts.
784
785 Args:
786 changed_files: List of modified files.
787 repository_root: The repository root.
788 default_presubmit: A default presubmit script to execute in any case.
789 verbose: Prints debug info.
790 output_stream: A stream to write debug output to.
791
792 Return:
793 List of try slaves
794 """
795 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
796 if not presubmit_files and verbose:
797 output_stream.write("Warning, no presubmit.py found.\n")
798 results = []
799 executer = GetTrySlavesExecuter()
800 if default_presubmit:
801 if verbose:
802 output_stream.write("Running default presubmit script.\n")
803 results += executer.ExecPresubmitScript(default_presubmit)
804 for filename in presubmit_files:
805 filename = os.path.abspath(filename)
806 if verbose:
807 output_stream.write("Running %s\n" % filename)
808 # Accept CRLF presubmit script.
809 presubmit_script = gcl.ReadFile(filename, 'rU')
810 results += executer.ExecPresubmitScript(presubmit_script)
811
812 slaves = list(set(results))
813 if slaves and verbose:
814 output_stream.write(', '.join(slaves))
815 output_stream.write('\n')
816 return slaves
817
818
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000819class PresubmitExecuter(object):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000820 def __init__(self, change, committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000821 """
822 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000823 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000824 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
825 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000826 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000827 self.committing = committing
828
829 def ExecPresubmitScript(self, script_text, presubmit_path):
830 """Executes a single presubmit script.
831
832 Args:
833 script_text: The text of the presubmit script.
834 presubmit_path: The path to the presubmit file (this will be reported via
835 input_api.PresubmitLocalPath()).
836
837 Return:
838 A list of result objects, empty if no problems.
839 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000840
841 # Change to the presubmit file's directory to support local imports.
842 main_path = os.getcwd()
843 os.chdir(os.path.dirname(presubmit_path))
844
845 # Load the presubmit script into context.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000846 input_api = InputApi(self.change, presubmit_path, self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000847 context = {}
848 exec script_text in context
849
850 # These function names must change if we make substantial changes to
851 # the presubmit API that are not backwards compatible.
852 if self.committing:
853 function_name = 'CheckChangeOnCommit'
854 else:
855 function_name = 'CheckChangeOnUpload'
856 if function_name in context:
857 context['__args'] = (input_api, OutputApi())
858 result = eval(function_name + '(*__args)', context)
859 if not (isinstance(result, types.TupleType) or
860 isinstance(result, types.ListType)):
861 raise exceptions.RuntimeError(
862 'Presubmit functions must return a tuple or list')
863 for item in result:
864 if not isinstance(item, OutputApi.PresubmitResult):
865 raise exceptions.RuntimeError(
866 'All presubmit results must be of types derived from '
867 'output_api.PresubmitResult')
868 else:
869 result = () # no error since the script doesn't care about current event.
870
chase@chromium.org8e416c82009-10-06 04:30:44 +0000871 # Return the process to the original working directory.
872 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000873 return result
874
875
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000876def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000877 committing,
878 verbose,
879 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000880 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000881 default_presubmit,
882 may_prompt):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000883 """Runs all presubmit checks that apply to the files in the change.
884
885 This finds all PRESUBMIT.py files in directories enclosing the files in the
886 change (up to the repository root) and calls the relevant entrypoint function
887 depending on whether the change is being committed or uploaded.
888
889 Prints errors, warnings and notifications. Prompts the user for warnings
890 when needed.
891
892 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000893 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000894 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
895 verbose: Prints debug info.
896 output_stream: A stream to write output from presubmit tests to.
897 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000898 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000899 may_prompt: Enable (y/n) questions on warning or error.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000900
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000901 Warning:
902 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
903 SHOULD be sys.stdin.
904
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000905 Return:
906 True if execution can continue, False if not.
907 """
jam@chromium.org2a891dc2009-08-20 20:33:37 +0000908 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000909 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
910 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000911 if not presubmit_files and verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000912 output_stream.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000913 results = []
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000914 executer = PresubmitExecuter(change, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000915 if default_presubmit:
916 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000917 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000918 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000919 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000920 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +0000921 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000922 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000923 output_stream.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +0000924 # Accept CRLF presubmit script.
maruel@chromium.org277003e2009-05-01 12:51:43 +0000925 presubmit_script = gcl.ReadFile(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000926 results += executer.ExecPresubmitScript(presubmit_script, filename)
927
928 errors = []
929 notifications = []
930 warnings = []
931 for result in results:
932 if not result.IsFatal() and not result.ShouldPrompt():
933 notifications.append(result)
934 elif result.ShouldPrompt():
935 warnings.append(result)
936 else:
937 errors.append(result)
938
939 error_count = 0
940 for name, items in (('Messages', notifications),
941 ('Warnings', warnings),
942 ('ERRORS', errors)):
943 if items:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000944 output_stream.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000945 for item in items:
946 if not item._Handle(output_stream, input_stream,
947 may_prompt=False):
948 error_count += 1
949 output_stream.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +0000950
951 total_time = time.time() - start_time
952 if total_time > 1.0:
953 print "Presubmit checks took %.1fs to calculate." % total_time
954
maruel@chromium.org07bbc212009-06-11 02:08:41 +0000955 if not errors and warnings and may_prompt:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000956 output_stream.write(
957 'There were presubmit warnings. Sure you want to continue? (y/N): ')
958 response = input_stream.readline()
959 if response.strip().lower() != 'y':
960 error_count += 1
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000961
962 global _ASKED_FOR_FEEDBACK
963 # Ask for feedback one time out of 5.
964 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
965 output_stream.write("Was the presubmit check useful? Please send feedback "
966 "& hate mail to maruel@chromium.org!\n")
967 _ASKED_FOR_FEEDBACK = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000968 return (error_count == 0)
969
970
971def ScanSubDirs(mask, recursive):
972 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000973 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 +0000974 else:
975 results = []
976 for root, dirs, files in os.walk('.'):
977 if '.svn' in dirs:
978 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000979 if '.git' in dirs:
980 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000981 for name in files:
982 if fnmatch.fnmatch(name, mask):
983 results.append(os.path.join(root, name))
984 return results
985
986
987def ParseFiles(args, recursive):
988 files = []
989 for arg in args:
990 files.extend([('M', file) for file in ScanSubDirs(arg, recursive)])
991 return files
992
993
994def Main(argv):
995 parser = optparse.OptionParser(usage="%prog [options]",
996 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000997 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000999 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1000 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001001 parser.add_option("-r", "--recursive", action="store_true",
1002 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001003 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001004 help="Verbose output")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001005 parser.add_option("--files")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001006 parser.add_option("--name", default='no name')
1007 parser.add_option("--description", default='')
1008 parser.add_option("--issue", type='int', default=0)
1009 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001010 parser.add_option("--root", default='')
1011 parser.add_option("--default_presubmit")
1012 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001013 options, args = parser.parse_args(argv[1:])
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001014 if not options.root:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001015 options.root = os.getcwd()
1016 if os.path.isdir(os.path.join(options.root, '.git')):
1017 change_class = GitChange
1018 if not options.files:
1019 if args:
1020 options.files = ParseFiles(args, options.recursive)
1021 else:
1022 # Grab modified files.
chase@chromium.org8e416c82009-10-06 04:30:44 +00001023 options.files = gclient_scm.CaptureGitStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001024 elif os.path.isdir(os.path.join(options.root, '.svn')):
1025 change_class = SvnChange
1026 if not options.files:
1027 if args:
1028 options.files = ParseFiles(args, options.recursive)
1029 else:
1030 # Grab modified files.
chase@chromium.org8e416c82009-10-06 04:30:44 +00001031 options.files = gclient_scm.CaptureSVNStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001032 else:
1033 # Doesn't seem under source control.
1034 change_class = Change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001035 if options.verbose:
chase@chromium.org8e416c82009-10-06 04:30:44 +00001036 if len(options.files) != 1:
1037 print "Found %d files." % len(options.files)
1038 else:
1039 print "Found 1 file."
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001040 return not DoPresubmitChecks(change_class(options.name,
1041 options.description,
1042 options.root,
1043 options.files,
1044 options.issue,
1045 options.patchset),
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001046 options.commit,
1047 options.verbose,
1048 sys.stdout,
1049 sys.stdin,
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001050 options.default_presubmit,
1051 options.may_prompt)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001052
1053
1054if __name__ == '__main__':
1055 sys.exit(Main(sys.argv))