blob: 3ab1549870fd16ddde4968c1615144b126acd805 [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.orgc1675e22009-04-27 20:30:48 +00009__version__ = '1.0.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 """
165 self.change = change
166
167 # We expose various modules and functions as attributes of the input_api
168 # so that presubmit scripts don't have to import them.
169 self.basename = os.path.basename
170 self.cPickle = cPickle
171 self.cStringIO = cStringIO
172 self.os_path = os.path
173 self.pickle = pickle
174 self.marshal = marshal
175 self.re = re
176 self.subprocess = subprocess
177 self.tempfile = tempfile
178 self.urllib2 = urllib2
179
180 # InputApi.platform is the platform you're currently running on.
181 self.platform = sys.platform
182
183 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000184 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000185
186 # We carry the canned checks so presubmit scripts can easily use them.
187 self.canned_checks = presubmit_canned_checks
188
189 def PresubmitLocalPath(self):
190 """Returns the local path of the presubmit script currently being run.
191
192 This is useful if you don't want to hard-code absolute paths in the
193 presubmit script. For example, It can be used to find another file
194 relative to the PRESUBMIT.py script, so the whole tree can be branched and
195 the presubmit script still works, without editing its content.
196 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000197 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000198
199 @staticmethod
200 def DepotToLocalPath(depot_path):
201 """Translate a depot path to a local path (relative to client root).
202
203 Args:
204 Depot path as a string.
205
206 Returns:
207 The local path of the depot path under the user's current client, or None
208 if the file is not mapped.
209
210 Remember to check for the None case and show an appropriate error!
211 """
maruel@chromium.org46a94102009-05-12 20:32:43 +0000212 local_path = gclient.CaptureSVNInfo(depot_path).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000213 if not local_path:
214 return None
215 else:
216 return local_path
217
218 @staticmethod
219 def LocalToDepotPath(local_path):
220 """Translate a local path to a depot path.
221
222 Args:
223 Local path (relative to current directory, or absolute) as a string.
224
225 Returns:
226 The depot path (SVN URL) of the file if mapped, otherwise None.
227 """
maruel@chromium.org46a94102009-05-12 20:32:43 +0000228 depot_path = gclient.CaptureSVNInfo(local_path).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000229 if not depot_path:
230 return None
231 else:
232 return depot_path
233
234 @staticmethod
235 def FilterTextFiles(affected_files, include_deletes=True):
236 """Filters out all except text files and optionally also filters out
237 deleted files.
238
239 Args:
240 affected_files: List of AffectedFiles objects.
241 include_deletes: If false, deleted files will be filtered out.
242
243 Returns:
244 Filtered list of AffectedFiles objects.
245 """
246 output_files = []
247 for af in affected_files:
248 if include_deletes or af.Action() != 'D':
249 path = af.AbsoluteLocalPath()
250 mime_type = gcl.GetSVNFileProperty(path, 'svn:mime-type')
251 if not mime_type or mime_type.startswith('text/'):
252 output_files.append(af)
253 return output_files
254
255 def AffectedFiles(self, include_dirs=False, include_deletes=True):
256 """Same as input_api.change.AffectedFiles() except only lists files
257 (and optionally directories) in the same directory as the current presubmit
258 script, or subdirectories thereof.
259 """
260 output_files = []
maruel@chromium.org3d235242009-05-15 12:40:48 +0000261 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000262 if len(dir_with_slash) == 1:
263 dir_with_slash = ''
264 for af in self.change.AffectedFiles(include_dirs, include_deletes):
265 af_path = normpath(af.LocalPath())
266 if af_path.startswith(dir_with_slash):
267 output_files.append(af)
268 return output_files
269
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
282 def AffectedTextFiles(self, include_deletes=True):
283 """Same as input_api.change.AffectedTextFiles() except only lists files
284 in the same directory as the current presubmit script, or subdirectories
285 thereof.
286
287 Warning: This function retrieves the svn property on each file so it can be
288 slow for large change lists.
289 """
290 return InputApi.FilterTextFiles(self.AffectedFiles(include_dirs=False),
291 include_deletes)
292
293 def RightHandSideLines(self):
294 """An iterator over all text lines in "new" version of changed files.
295
296 Only lists lines from new or modified text files in the change that are
297 contained by the directory of the currently executing presubmit script.
298
299 This is useful for doing line-by-line regex checks, like checking for
300 trailing whitespace.
301
302 Yields:
303 a 3 tuple:
304 the AffectedFile instance of the current file;
305 integer line number (1-based); and
306 the contents of the line as a string.
307 """
308 return InputApi._RightHandSideLinesImpl(
309 self.AffectedTextFiles(include_deletes=False))
310
311 @staticmethod
312 def _RightHandSideLinesImpl(affected_files):
313 """Implements RightHandSideLines for InputApi and GclChange."""
314 for af in affected_files:
315 lines = af.NewContents()
316 line_number = 0
317 for line in lines:
318 line_number += 1
319 yield (af, line_number, line)
320
321
322class AffectedFile(object):
323 """Representation of a file in a change."""
324
325 def __init__(self, path, action, repository_root=''):
326 self.path = path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000327 self.action = action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000328 self.repository_root = repository_root
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000329 self.server_path = None
330 self.is_directory = None
331 self.properties = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000332
333 def ServerPath(self):
334 """Returns a path string that identifies the file in the SCM system.
335
336 Returns the empty string if the file does not exist in SCM.
337 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000338 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000339
340 def LocalPath(self):
341 """Returns the path of this file on the local disk relative to client root.
342 """
343 return normpath(self.path)
344
345 def AbsoluteLocalPath(self):
346 """Returns the absolute path of this file on the local disk.
347 """
348 return normpath(os.path.join(self.repository_root, self.LocalPath()))
349
350 def IsDirectory(self):
351 """Returns true if this object is a directory."""
maruel@chromium.org73e36ed2009-05-22 20:04:13 +0000352 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000353 if self.is_directory is None:
354 self.is_directory = (os.path.exists(path) and
355 os.path.isdir(path))
356 return self.is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000357
358 def Action(self):
359 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000360 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
361 # different for other SCM.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000362 return self.action
363
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000364 def Property(self, property_name):
365 """Returns the specified SCM property of this file, or None if no such
366 property.
367 """
368 return self.properties.get(property_name, None)
369
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000370 def NewContents(self):
371 """Returns an iterator over the lines in the new version of file.
372
373 The new version is the file in the user's workspace, i.e. the "right hand
374 side".
375
376 Contents will be empty if the file is a directory or does not exist.
377 """
378 if self.IsDirectory():
379 return []
380 else:
381 return gcl.ReadFile(self.AbsoluteLocalPath()).splitlines()
382
383 def OldContents(self):
384 """Returns an iterator over the lines in the old version of file.
385
386 The old version is the file in depot, i.e. the "left hand side".
387 """
388 raise NotImplementedError() # Implement when needed
389
390 def OldFileTempPath(self):
391 """Returns the path on local disk where the old contents resides.
392
393 The old version is the file in depot, i.e. the "left hand side".
394 This is a read-only cached copy of the old contents. *DO NOT* try to
395 modify this file.
396 """
397 raise NotImplementedError() # Implement if/when needed.
398
399
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000400class SvnAffectedFile(AffectedFile):
401 """Representation of a file in a change out of a Subversion checkout."""
402
403 def ServerPath(self):
404 if self.server_path is None:
405 self.server_path = gclient.CaptureSVNInfo(
406 self.AbsoluteLocalPath()).get('URL', '')
407 return self.server_path
408
409 def IsDirectory(self):
410 path = self.AbsoluteLocalPath()
411 if self.is_directory is None:
412 if os.path.exists(path):
413 # Retrieve directly from the file system; it is much faster than
414 # querying subversion, especially on Windows.
415 self.is_directory = os.path.isdir(path)
416 else:
417 self.is_directory = gclient.CaptureSVNInfo(
418 path).get('Node Kind') in ('dir', 'directory')
419 return self.is_directory
420
421 def Property(self, property_name):
422 if not property_name in self.properties:
423 self.properties[property_name] = gcl.GetSVNFileProperty(
424 self.AbsoluteLocalPath(), property_name)
425 return self.properties[property_name]
426
427
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000428class GclChange(object):
429 """A gcl change. See gcl.ChangeInfo for more info."""
430
431 def __init__(self, change_info, repository_root=''):
432 self.name = change_info.name
433 self.full_description = change_info.description
434 self.repository_root = repository_root
435
436 # From the description text, build up a dictionary of key/value pairs
437 # plus the description minus all key/value or "tag" lines.
438 self.description_without_tags = []
439 self.tags = {}
440 for line in change_info.description.splitlines():
441 m = _tag_line_re.match(line)
442 if m:
443 self.tags[m.group('key')] = m.group('value')
444 else:
445 self.description_without_tags.append(line)
446
447 # Change back to text and remove whitespace at end.
448 self.description_without_tags = '\n'.join(self.description_without_tags)
449 self.description_without_tags = self.description_without_tags.rstrip()
450
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000451 self.affected_files = [
452 SvnAffectedFile(info[1], info[0].strip(), repository_root)
453 for info in change_info.files
454 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000455
456 def Change(self):
457 """Returns the change name."""
458 return self.name
459
460 def Changelist(self):
461 """Synonym for Change()."""
462 return self.Change()
463
464 def DescriptionText(self):
465 """Returns the user-entered changelist description, minus tags.
466
467 Any line in the user-provided description starting with e.g. "FOO="
468 (whitespace permitted before and around) is considered a tag line. Such
469 lines are stripped out of the description this function returns.
470 """
471 return self.description_without_tags
472
473 def FullDescriptionText(self):
474 """Returns the complete changelist description including tags."""
475 return self.full_description
476
477 def RepositoryRoot(self):
478 """Returns the repository root for this change, as an absolute path."""
479 return self.repository_root
480
481 def __getattr__(self, attr):
482 """Return keys directly as attributes on the object.
483
484 You may use a friendly name (from SPECIAL_KEYS) or the actual name of
485 the key.
486 """
487 if attr in SPECIAL_KEYS:
488 key = SPECIAL_KEYS[attr]
489 if key in self.tags:
490 return self.tags[key]
491 if attr in self.tags:
492 return self.tags[attr]
493
494 def AffectedFiles(self, include_dirs=False, include_deletes=True):
495 """Returns a list of AffectedFile instances for all files in the change.
496
497 Args:
498 include_deletes: If false, deleted files will be filtered out.
499 include_dirs: True to include directories in the list
500
501 Returns:
502 [AffectedFile(path, action), AffectedFile(path, action)]
503 """
504 if include_dirs:
505 affected = self.affected_files
506 else:
507 affected = filter(lambda x: not x.IsDirectory(), self.affected_files)
508
509 if include_deletes:
510 return affected
511 else:
512 return filter(lambda x: x.Action() != 'D', affected)
513
514 def AffectedTextFiles(self, include_deletes=True):
515 """Return a list of the text files in a change.
516
517 It's common to want to iterate over only the text files.
518
519 Args:
520 include_deletes: Controls whether to return files with "delete" actions,
521 which commonly aren't relevant to presubmit scripts.
522 """
523 return InputApi.FilterTextFiles(self.AffectedFiles(include_dirs=False),
524 include_deletes)
525
526 def LocalPaths(self, include_dirs=False):
527 """Convenience function."""
528 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
529
530 def AbsoluteLocalPaths(self, include_dirs=False):
531 """Convenience function."""
532 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
533
534 def ServerPaths(self, include_dirs=False):
535 """Convenience function."""
536 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
537
538 def RightHandSideLines(self):
539 """An iterator over all text lines in "new" version of changed files.
540
541 Lists lines from new or modified text files in the change.
542
543 This is useful for doing line-by-line regex checks, like checking for
544 trailing whitespace.
545
546 Yields:
547 a 3 tuple:
548 the AffectedFile instance of the current file;
549 integer line number (1-based); and
550 the contents of the line as a string.
551 """
552 return InputApi._RightHandSideLinesImpl(
553 self.AffectedTextFiles(include_deletes=False))
554
555
556def ListRelevantPresubmitFiles(files):
557 """Finds all presubmit files that apply to a given set of source files.
558
559 Args:
560 files: An iterable container containing file paths.
561
562 Return:
563 ['foo/blat/PRESUBMIT.py', 'mat/gat/PRESUBMIT.py']
564 """
565 checked_dirs = {} # Keys are directory paths, values are ignored.
566 source_dirs = [os.path.dirname(f) for f in files]
567 presubmit_files = []
568 for dir in source_dirs:
569 while (True):
570 if dir in checked_dirs:
571 break # We've already walked up from this directory.
572
573 test_path = os.path.join(dir, 'PRESUBMIT.py')
574 if os.path.isfile(test_path):
575 presubmit_files.append(normpath(test_path))
576
577 checked_dirs[dir] = ''
578 if dir in ['', '.']:
579 break
580 else:
581 dir = os.path.dirname(dir)
582 return presubmit_files
583
584
585class PresubmitExecuter(object):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000586 def __init__(self, change_info, committing):
587 """
588 Args:
589 change_info: The ChangeInfo object for the change.
590 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
591 """
592 self.change = GclChange(change_info, gcl.GetRepositoryRoot())
593 self.committing = committing
594
595 def ExecPresubmitScript(self, script_text, presubmit_path):
596 """Executes a single presubmit script.
597
598 Args:
599 script_text: The text of the presubmit script.
600 presubmit_path: The path to the presubmit file (this will be reported via
601 input_api.PresubmitLocalPath()).
602
603 Return:
604 A list of result objects, empty if no problems.
605 """
606 input_api = InputApi(self.change, presubmit_path)
607 context = {}
608 exec script_text in context
609
610 # These function names must change if we make substantial changes to
611 # the presubmit API that are not backwards compatible.
612 if self.committing:
613 function_name = 'CheckChangeOnCommit'
614 else:
615 function_name = 'CheckChangeOnUpload'
616 if function_name in context:
617 context['__args'] = (input_api, OutputApi())
618 result = eval(function_name + '(*__args)', context)
619 if not (isinstance(result, types.TupleType) or
620 isinstance(result, types.ListType)):
621 raise exceptions.RuntimeError(
622 'Presubmit functions must return a tuple or list')
623 for item in result:
624 if not isinstance(item, OutputApi.PresubmitResult):
625 raise exceptions.RuntimeError(
626 'All presubmit results must be of types derived from '
627 'output_api.PresubmitResult')
628 else:
629 result = () # no error since the script doesn't care about current event.
630
631 return result
632
633
634def DoPresubmitChecks(change_info,
635 committing,
636 verbose,
637 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000638 input_stream,
639 default_presubmit):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000640 """Runs all presubmit checks that apply to the files in the change.
641
642 This finds all PRESUBMIT.py files in directories enclosing the files in the
643 change (up to the repository root) and calls the relevant entrypoint function
644 depending on whether the change is being committed or uploaded.
645
646 Prints errors, warnings and notifications. Prompts the user for warnings
647 when needed.
648
649 Args:
650 change_info: The ChangeInfo object for the change.
651 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
652 verbose: Prints debug info.
653 output_stream: A stream to write output from presubmit tests to.
654 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000655 default_presubmit: A default presubmit script to execute in any case.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000656
657 Return:
658 True if execution can continue, False if not.
659 """
660 presubmit_files = ListRelevantPresubmitFiles(change_info.FileList())
661 if not presubmit_files and verbose:
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000662 output_stream.write("Warning, no presubmit.py found.")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000663 results = []
664 executer = PresubmitExecuter(change_info, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000665 if default_presubmit:
666 if verbose:
667 output_stream.write("Running default presubmit script")
668 results += executer.ExecPresubmitScript(default_presubmit, 'PRESUBMIT.py')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000669 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +0000670 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000671 if verbose:
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000672 output_stream.write("Running %s" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +0000673 # Accept CRLF presubmit script.
maruel@chromium.org277003e2009-05-01 12:51:43 +0000674 presubmit_script = gcl.ReadFile(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000675 results += executer.ExecPresubmitScript(presubmit_script, filename)
676
677 errors = []
678 notifications = []
679 warnings = []
680 for result in results:
681 if not result.IsFatal() and not result.ShouldPrompt():
682 notifications.append(result)
683 elif result.ShouldPrompt():
684 warnings.append(result)
685 else:
686 errors.append(result)
687
688 error_count = 0
689 for name, items in (('Messages', notifications),
690 ('Warnings', warnings),
691 ('ERRORS', errors)):
692 if items:
693 output_stream.write('\n** Presubmit %s **\n\n' % name)
694 for item in items:
695 if not item._Handle(output_stream, input_stream,
696 may_prompt=False):
697 error_count += 1
698 output_stream.write('\n')
699 if not errors and warnings:
700 output_stream.write(
701 'There were presubmit warnings. Sure you want to continue? (y/N): ')
702 response = input_stream.readline()
703 if response.strip().lower() != 'y':
704 error_count += 1
705 return (error_count == 0)
706
707
708def ScanSubDirs(mask, recursive):
709 if not recursive:
710 return [x for x in glob.glob(mask) if '.svn' not in x]
711 else:
712 results = []
713 for root, dirs, files in os.walk('.'):
714 if '.svn' in dirs:
715 dirs.remove('.svn')
716 for name in files:
717 if fnmatch.fnmatch(name, mask):
718 results.append(os.path.join(root, name))
719 return results
720
721
722def ParseFiles(args, recursive):
723 files = []
724 for arg in args:
725 files.extend([('M', file) for file in ScanSubDirs(arg, recursive)])
726 return files
727
728
729def Main(argv):
730 parser = optparse.OptionParser(usage="%prog [options]",
731 version="%prog " + str(__version__))
732 parser.add_option("-c", "--commit", action="store_true",
733 help="Use commit instead of upload checks")
734 parser.add_option("-r", "--recursive", action="store_true",
735 help="Act recursively")
736 parser.add_option("-v", "--verbose", action="store_true",
737 help="Verbose output")
738 options, args = parser.parse_args(argv[1:])
739 files = ParseFiles(args, options.recursive)
740 if options.verbose:
741 print "Found %d files." % len(files)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000742 return not DoPresubmitChecks(gcl.ChangeInfo(name='temp', files=files),
743 options.commit,
744 options.verbose,
745 sys.stdout,
746 sys.stdin,
747 default_presubmit=None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000748
749
750if __name__ == '__main__':
751 sys.exit(Main(sys.argv))