blob: d4361d4958908a3e5bf21d32758f104d4f2c1f65 [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.org9711bba2009-05-22 23:51:39 +00009__version__ = '1.1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000010
11# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
12# caching (between all different invocations of presubmit scripts for a given
13# change). We should add it as our presubmit scripts start feeling slow.
14
15import cPickle # Exposed through the API.
16import cStringIO # Exposed through the API.
17import exceptions
18import fnmatch
19import glob
20import marshal # Exposed through the API.
21import optparse
22import os # Somewhat exposed through the API.
23import pickle # Exposed through the API.
24import re # Exposed through the API.
25import subprocess # Exposed through the API.
26import sys # Parts exposed through API.
27import tempfile # Exposed through the API.
28import types
29import urllib2 # Exposed through the API.
30
31# Local imports.
32# TODO(joi) Would be cleaner to factor out utils in gcl to separate module, but
33# for now it would only be a couple of functions so hardly worth it.
34import gcl
maruel@chromium.org46a94102009-05-12 20:32:43 +000035import gclient
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000036import presubmit_canned_checks
37
38
39# Matches key/value (or "tag") lines in changelist descriptions.
40_tag_line_re = re.compile(
41 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
42
43
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000044class NotImplementedException(Exception):
45 """We're leaving placeholders in a bunch of places to remind us of the
46 design of the API, but we have not implemented all of it yet. Implement as
47 the need arises.
48 """
49 pass
50
51
52def normpath(path):
53 '''Version of os.path.normpath that also changes backward slashes to
54 forward slashes when not running on Windows.
55 '''
56 # This is safe to always do because the Windows version of os.path.normpath
57 # will replace forward slashes with backward slashes.
58 path = path.replace(os.sep, '/')
59 return os.path.normpath(path)
60
61
62
63class OutputApi(object):
64 """This class (more like a module) gets passed to presubmit scripts so that
65 they can specify various types of results.
66 """
67
68 class PresubmitResult(object):
69 """Base class for result objects."""
70
71 def __init__(self, message, items=None, long_text=''):
72 """
73 message: A short one-line message to indicate errors.
74 items: A list of short strings to indicate where errors occurred.
75 long_text: multi-line text output, e.g. from another tool
76 """
77 self._message = message
78 self._items = []
79 if items:
80 self._items = items
81 self._long_text = long_text.rstrip()
82
83 def _Handle(self, output_stream, input_stream, may_prompt=True):
84 """Writes this result to the output stream.
85
86 Args:
87 output_stream: Where to write
88
89 Returns:
90 True if execution may continue, False otherwise.
91 """
92 output_stream.write(self._message)
93 output_stream.write('\n')
94 for item in self._items:
95 output_stream.write(' %s\n' % item)
96 if self._long_text:
97 output_stream.write('\n***************\n%s\n***************\n\n' %
98 self._long_text)
99
100 if self.ShouldPrompt() and may_prompt:
101 output_stream.write('Are you sure you want to continue? (y/N): ')
102 response = input_stream.readline()
103 if response.strip().lower() != 'y':
104 return False
105
106 return not self.IsFatal()
107
108 def IsFatal(self):
109 """An error that is fatal stops g4 mail/submit immediately, i.e. before
110 other presubmit scripts are run.
111 """
112 return False
113
114 def ShouldPrompt(self):
115 """Whether this presubmit result should result in a prompt warning."""
116 return False
117
118 class PresubmitError(PresubmitResult):
119 """A hard presubmit error."""
120 def IsFatal(self):
121 return True
122
123 class PresubmitPromptWarning(PresubmitResult):
124 """An warning that prompts the user if they want to continue."""
125 def ShouldPrompt(self):
126 return True
127
128 class PresubmitNotifyResult(PresubmitResult):
129 """Just print something to the screen -- but it's not even a warning."""
130 pass
131
132 class MailTextResult(PresubmitResult):
133 """A warning that should be included in the review request email."""
134 def __init__(self, *args, **kwargs):
135 raise NotImplementedException() # TODO(joi) Implement.
136
137
138class InputApi(object):
139 """An instance of this object is passed to presubmit scripts so they can
140 know stuff about the change they're looking at.
141 """
142
143 def __init__(self, change, presubmit_path):
144 """Builds an InputApi object.
145
146 Args:
147 change: A presubmit.GclChange object.
148 presubmit_path: The path to the presubmit script being processed.
149 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000150 # Version number of the presubmit_support script.
151 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000152 self.change = change
153
154 # We expose various modules and functions as attributes of the input_api
155 # so that presubmit scripts don't have to import them.
156 self.basename = os.path.basename
157 self.cPickle = cPickle
158 self.cStringIO = cStringIO
159 self.os_path = os.path
160 self.pickle = pickle
161 self.marshal = marshal
162 self.re = re
163 self.subprocess = subprocess
164 self.tempfile = tempfile
165 self.urllib2 = urllib2
166
167 # InputApi.platform is the platform you're currently running on.
168 self.platform = sys.platform
169
170 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000171 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000172
173 # We carry the canned checks so presubmit scripts can easily use them.
174 self.canned_checks = presubmit_canned_checks
175
176 def PresubmitLocalPath(self):
177 """Returns the local path of the presubmit script currently being run.
178
179 This is useful if you don't want to hard-code absolute paths in the
180 presubmit script. For example, It can be used to find another file
181 relative to the PRESUBMIT.py script, so the whole tree can be branched and
182 the presubmit script still works, without editing its content.
183 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000184 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000185
186 @staticmethod
187 def DepotToLocalPath(depot_path):
188 """Translate a depot path to a local path (relative to client root).
189
190 Args:
191 Depot path as a string.
192
193 Returns:
194 The local path of the depot path under the user's current client, or None
195 if the file is not mapped.
196
197 Remember to check for the None case and show an appropriate error!
198 """
maruel@chromium.org46a94102009-05-12 20:32:43 +0000199 local_path = gclient.CaptureSVNInfo(depot_path).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000200 if not local_path:
201 return None
202 else:
203 return local_path
204
205 @staticmethod
206 def LocalToDepotPath(local_path):
207 """Translate a local path to a depot path.
208
209 Args:
210 Local path (relative to current directory, or absolute) as a string.
211
212 Returns:
213 The depot path (SVN URL) of the file if mapped, otherwise None.
214 """
maruel@chromium.org46a94102009-05-12 20:32:43 +0000215 depot_path = gclient.CaptureSVNInfo(local_path).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000216 if not depot_path:
217 return None
218 else:
219 return depot_path
220
221 @staticmethod
222 def FilterTextFiles(affected_files, include_deletes=True):
223 """Filters out all except text files and optionally also filters out
224 deleted files.
225
226 Args:
227 affected_files: List of AffectedFiles objects.
228 include_deletes: If false, deleted files will be filtered out.
229
230 Returns:
231 Filtered list of AffectedFiles objects.
232 """
233 output_files = []
234 for af in affected_files:
235 if include_deletes or af.Action() != 'D':
236 path = af.AbsoluteLocalPath()
237 mime_type = gcl.GetSVNFileProperty(path, 'svn:mime-type')
238 if not mime_type or mime_type.startswith('text/'):
239 output_files.append(af)
240 return output_files
241
242 def AffectedFiles(self, include_dirs=False, include_deletes=True):
243 """Same as input_api.change.AffectedFiles() except only lists files
244 (and optionally directories) in the same directory as the current presubmit
245 script, or subdirectories thereof.
246 """
247 output_files = []
maruel@chromium.org3d235242009-05-15 12:40:48 +0000248 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000249 if len(dir_with_slash) == 1:
250 dir_with_slash = ''
251 for af in self.change.AffectedFiles(include_dirs, include_deletes):
252 af_path = normpath(af.LocalPath())
253 if af_path.startswith(dir_with_slash):
254 output_files.append(af)
255 return output_files
256
257 def LocalPaths(self, include_dirs=False):
258 """Returns local paths of input_api.AffectedFiles()."""
259 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
260
261 def AbsoluteLocalPaths(self, include_dirs=False):
262 """Returns absolute local paths of input_api.AffectedFiles()."""
263 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
264
265 def ServerPaths(self, include_dirs=False):
266 """Returns server paths of input_api.AffectedFiles()."""
267 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
268
269 def AffectedTextFiles(self, include_deletes=True):
270 """Same as input_api.change.AffectedTextFiles() except only lists files
271 in the same directory as the current presubmit script, or subdirectories
272 thereof.
273
274 Warning: This function retrieves the svn property on each file so it can be
275 slow for large change lists.
276 """
277 return InputApi.FilterTextFiles(self.AffectedFiles(include_dirs=False),
278 include_deletes)
279
280 def RightHandSideLines(self):
281 """An iterator over all text lines in "new" version of changed files.
282
283 Only lists lines from new or modified text files in the change that are
284 contained by the directory of the currently executing presubmit script.
285
286 This is useful for doing line-by-line regex checks, like checking for
287 trailing whitespace.
288
289 Yields:
290 a 3 tuple:
291 the AffectedFile instance of the current file;
292 integer line number (1-based); and
293 the contents of the line as a string.
294 """
295 return InputApi._RightHandSideLinesImpl(
296 self.AffectedTextFiles(include_deletes=False))
297
298 @staticmethod
299 def _RightHandSideLinesImpl(affected_files):
300 """Implements RightHandSideLines for InputApi and GclChange."""
301 for af in affected_files:
302 lines = af.NewContents()
303 line_number = 0
304 for line in lines:
305 line_number += 1
306 yield (af, line_number, line)
307
308
309class AffectedFile(object):
310 """Representation of a file in a change."""
311
312 def __init__(self, path, action, repository_root=''):
313 self.path = path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000314 self.action = action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000315 self.repository_root = repository_root
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000316 self.server_path = None
317 self.is_directory = None
318 self.properties = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000319
320 def ServerPath(self):
321 """Returns a path string that identifies the file in the SCM system.
322
323 Returns the empty string if the file does not exist in SCM.
324 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000325 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000326
327 def LocalPath(self):
328 """Returns the path of this file on the local disk relative to client root.
329 """
330 return normpath(self.path)
331
332 def AbsoluteLocalPath(self):
333 """Returns the absolute path of this file on the local disk.
334 """
335 return normpath(os.path.join(self.repository_root, self.LocalPath()))
336
337 def IsDirectory(self):
338 """Returns true if this object is a directory."""
maruel@chromium.org73e36ed2009-05-22 20:04:13 +0000339 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000340 if self.is_directory is None:
341 self.is_directory = (os.path.exists(path) and
342 os.path.isdir(path))
343 return self.is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000344
345 def Action(self):
346 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000347 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
348 # different for other SCM.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000349 return self.action
350
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000351 def Property(self, property_name):
352 """Returns the specified SCM property of this file, or None if no such
353 property.
354 """
355 return self.properties.get(property_name, None)
356
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000357 def NewContents(self):
358 """Returns an iterator over the lines in the new version of file.
359
360 The new version is the file in the user's workspace, i.e. the "right hand
361 side".
362
363 Contents will be empty if the file is a directory or does not exist.
364 """
365 if self.IsDirectory():
366 return []
367 else:
368 return gcl.ReadFile(self.AbsoluteLocalPath()).splitlines()
369
370 def OldContents(self):
371 """Returns an iterator over the lines in the old version of file.
372
373 The old version is the file in depot, i.e. the "left hand side".
374 """
375 raise NotImplementedError() # Implement when needed
376
377 def OldFileTempPath(self):
378 """Returns the path on local disk where the old contents resides.
379
380 The old version is the file in depot, i.e. the "left hand side".
381 This is a read-only cached copy of the old contents. *DO NOT* try to
382 modify this file.
383 """
384 raise NotImplementedError() # Implement if/when needed.
385
386
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000387class SvnAffectedFile(AffectedFile):
388 """Representation of a file in a change out of a Subversion checkout."""
389
390 def ServerPath(self):
391 if self.server_path is None:
392 self.server_path = gclient.CaptureSVNInfo(
393 self.AbsoluteLocalPath()).get('URL', '')
394 return self.server_path
395
396 def IsDirectory(self):
397 path = self.AbsoluteLocalPath()
398 if self.is_directory is None:
399 if os.path.exists(path):
400 # Retrieve directly from the file system; it is much faster than
401 # querying subversion, especially on Windows.
402 self.is_directory = os.path.isdir(path)
403 else:
404 self.is_directory = gclient.CaptureSVNInfo(
405 path).get('Node Kind') in ('dir', 'directory')
406 return self.is_directory
407
408 def Property(self, property_name):
409 if not property_name in self.properties:
410 self.properties[property_name] = gcl.GetSVNFileProperty(
411 self.AbsoluteLocalPath(), property_name)
412 return self.properties[property_name]
413
414
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000415class GclChange(object):
416 """A gcl change. See gcl.ChangeInfo for more info."""
417
418 def __init__(self, change_info, repository_root=''):
419 self.name = change_info.name
420 self.full_description = change_info.description
421 self.repository_root = repository_root
422
423 # From the description text, build up a dictionary of key/value pairs
424 # plus the description minus all key/value or "tag" lines.
425 self.description_without_tags = []
426 self.tags = {}
427 for line in change_info.description.splitlines():
428 m = _tag_line_re.match(line)
429 if m:
430 self.tags[m.group('key')] = m.group('value')
431 else:
432 self.description_without_tags.append(line)
433
434 # Change back to text and remove whitespace at end.
435 self.description_without_tags = '\n'.join(self.description_without_tags)
436 self.description_without_tags = self.description_without_tags.rstrip()
437
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000438 self.affected_files = [
439 SvnAffectedFile(info[1], info[0].strip(), repository_root)
440 for info in change_info.files
441 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000442
443 def Change(self):
444 """Returns the change name."""
445 return self.name
446
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000447 def DescriptionText(self):
448 """Returns the user-entered changelist description, minus tags.
449
450 Any line in the user-provided description starting with e.g. "FOO="
451 (whitespace permitted before and around) is considered a tag line. Such
452 lines are stripped out of the description this function returns.
453 """
454 return self.description_without_tags
455
456 def FullDescriptionText(self):
457 """Returns the complete changelist description including tags."""
458 return self.full_description
459
460 def RepositoryRoot(self):
461 """Returns the repository root for this change, as an absolute path."""
462 return self.repository_root
463
464 def __getattr__(self, attr):
465 """Return keys directly as attributes on the object.
466
467 You may use a friendly name (from SPECIAL_KEYS) or the actual name of
468 the key.
469 """
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000470 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000471
472 def AffectedFiles(self, include_dirs=False, include_deletes=True):
473 """Returns a list of AffectedFile instances for all files in the change.
474
475 Args:
476 include_deletes: If false, deleted files will be filtered out.
477 include_dirs: True to include directories in the list
478
479 Returns:
480 [AffectedFile(path, action), AffectedFile(path, action)]
481 """
482 if include_dirs:
483 affected = self.affected_files
484 else:
485 affected = filter(lambda x: not x.IsDirectory(), self.affected_files)
486
487 if include_deletes:
488 return affected
489 else:
490 return filter(lambda x: x.Action() != 'D', affected)
491
492 def AffectedTextFiles(self, include_deletes=True):
493 """Return a list of the text files in a change.
494
495 It's common to want to iterate over only the text files.
496
497 Args:
498 include_deletes: Controls whether to return files with "delete" actions,
499 which commonly aren't relevant to presubmit scripts.
500 """
501 return InputApi.FilterTextFiles(self.AffectedFiles(include_dirs=False),
502 include_deletes)
503
504 def LocalPaths(self, include_dirs=False):
505 """Convenience function."""
506 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
507
508 def AbsoluteLocalPaths(self, include_dirs=False):
509 """Convenience function."""
510 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
511
512 def ServerPaths(self, include_dirs=False):
513 """Convenience function."""
514 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
515
516 def RightHandSideLines(self):
517 """An iterator over all text lines in "new" version of changed files.
518
519 Lists lines from new or modified text files in the change.
520
521 This is useful for doing line-by-line regex checks, like checking for
522 trailing whitespace.
523
524 Yields:
525 a 3 tuple:
526 the AffectedFile instance of the current file;
527 integer line number (1-based); and
528 the contents of the line as a string.
529 """
530 return InputApi._RightHandSideLinesImpl(
531 self.AffectedTextFiles(include_deletes=False))
532
533
534def ListRelevantPresubmitFiles(files):
535 """Finds all presubmit files that apply to a given set of source files.
536
537 Args:
538 files: An iterable container containing file paths.
539
540 Return:
541 ['foo/blat/PRESUBMIT.py', 'mat/gat/PRESUBMIT.py']
542 """
543 checked_dirs = {} # Keys are directory paths, values are ignored.
544 source_dirs = [os.path.dirname(f) for f in files]
545 presubmit_files = []
546 for dir in source_dirs:
547 while (True):
548 if dir in checked_dirs:
549 break # We've already walked up from this directory.
550
551 test_path = os.path.join(dir, 'PRESUBMIT.py')
552 if os.path.isfile(test_path):
553 presubmit_files.append(normpath(test_path))
554
555 checked_dirs[dir] = ''
556 if dir in ['', '.']:
557 break
558 else:
559 dir = os.path.dirname(dir)
560 return presubmit_files
561
562
563class PresubmitExecuter(object):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000564 def __init__(self, change_info, committing):
565 """
566 Args:
567 change_info: The ChangeInfo object for the change.
568 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
569 """
570 self.change = GclChange(change_info, gcl.GetRepositoryRoot())
571 self.committing = committing
572
573 def ExecPresubmitScript(self, script_text, presubmit_path):
574 """Executes a single presubmit script.
575
576 Args:
577 script_text: The text of the presubmit script.
578 presubmit_path: The path to the presubmit file (this will be reported via
579 input_api.PresubmitLocalPath()).
580
581 Return:
582 A list of result objects, empty if no problems.
583 """
584 input_api = InputApi(self.change, presubmit_path)
585 context = {}
586 exec script_text in context
587
588 # These function names must change if we make substantial changes to
589 # the presubmit API that are not backwards compatible.
590 if self.committing:
591 function_name = 'CheckChangeOnCommit'
592 else:
593 function_name = 'CheckChangeOnUpload'
594 if function_name in context:
595 context['__args'] = (input_api, OutputApi())
596 result = eval(function_name + '(*__args)', context)
597 if not (isinstance(result, types.TupleType) or
598 isinstance(result, types.ListType)):
599 raise exceptions.RuntimeError(
600 'Presubmit functions must return a tuple or list')
601 for item in result:
602 if not isinstance(item, OutputApi.PresubmitResult):
603 raise exceptions.RuntimeError(
604 'All presubmit results must be of types derived from '
605 'output_api.PresubmitResult')
606 else:
607 result = () # no error since the script doesn't care about current event.
608
609 return result
610
611
612def DoPresubmitChecks(change_info,
613 committing,
614 verbose,
615 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000616 input_stream,
617 default_presubmit):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000618 """Runs all presubmit checks that apply to the files in the change.
619
620 This finds all PRESUBMIT.py files in directories enclosing the files in the
621 change (up to the repository root) and calls the relevant entrypoint function
622 depending on whether the change is being committed or uploaded.
623
624 Prints errors, warnings and notifications. Prompts the user for warnings
625 when needed.
626
627 Args:
628 change_info: The ChangeInfo object for the change.
629 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
630 verbose: Prints debug info.
631 output_stream: A stream to write output from presubmit tests to.
632 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000633 default_presubmit: A default presubmit script to execute in any case.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000634
635 Return:
636 True if execution can continue, False if not.
637 """
638 presubmit_files = ListRelevantPresubmitFiles(change_info.FileList())
639 if not presubmit_files and verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000640 output_stream.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000641 results = []
642 executer = PresubmitExecuter(change_info, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000643 if default_presubmit:
644 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000645 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000646 results += executer.ExecPresubmitScript(default_presubmit, 'PRESUBMIT.py')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000647 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +0000648 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000649 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000650 output_stream.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +0000651 # Accept CRLF presubmit script.
maruel@chromium.org277003e2009-05-01 12:51:43 +0000652 presubmit_script = gcl.ReadFile(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000653 results += executer.ExecPresubmitScript(presubmit_script, filename)
654
655 errors = []
656 notifications = []
657 warnings = []
658 for result in results:
659 if not result.IsFatal() and not result.ShouldPrompt():
660 notifications.append(result)
661 elif result.ShouldPrompt():
662 warnings.append(result)
663 else:
664 errors.append(result)
665
666 error_count = 0
667 for name, items in (('Messages', notifications),
668 ('Warnings', warnings),
669 ('ERRORS', errors)):
670 if items:
671 output_stream.write('\n** Presubmit %s **\n\n' % name)
672 for item in items:
673 if not item._Handle(output_stream, input_stream,
674 may_prompt=False):
675 error_count += 1
676 output_stream.write('\n')
677 if not errors and warnings:
678 output_stream.write(
679 'There were presubmit warnings. Sure you want to continue? (y/N): ')
680 response = input_stream.readline()
681 if response.strip().lower() != 'y':
682 error_count += 1
683 return (error_count == 0)
684
685
686def ScanSubDirs(mask, recursive):
687 if not recursive:
688 return [x for x in glob.glob(mask) if '.svn' not in x]
689 else:
690 results = []
691 for root, dirs, files in os.walk('.'):
692 if '.svn' in dirs:
693 dirs.remove('.svn')
694 for name in files:
695 if fnmatch.fnmatch(name, mask):
696 results.append(os.path.join(root, name))
697 return results
698
699
700def ParseFiles(args, recursive):
701 files = []
702 for arg in args:
703 files.extend([('M', file) for file in ScanSubDirs(arg, recursive)])
704 return files
705
706
707def Main(argv):
708 parser = optparse.OptionParser(usage="%prog [options]",
709 version="%prog " + str(__version__))
710 parser.add_option("-c", "--commit", action="store_true",
711 help="Use commit instead of upload checks")
712 parser.add_option("-r", "--recursive", action="store_true",
713 help="Act recursively")
714 parser.add_option("-v", "--verbose", action="store_true",
715 help="Verbose output")
716 options, args = parser.parse_args(argv[1:])
717 files = ParseFiles(args, options.recursive)
718 if options.verbose:
719 print "Found %d files." % len(files)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000720 return not DoPresubmitChecks(gcl.ChangeInfo(name='temp', files=files),
721 options.commit,
722 options.verbose,
723 sys.stdout,
724 sys.stdin,
725 default_presubmit=None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000726
727
728if __name__ == '__main__':
729 sys.exit(Main(sys.argv))