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