blob: 9c3a6e817e4acd4eba168011ecf7b599aec4822f [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
327 self.action = action.strip()
328 self.repository_root = repository_root
329
330 def ServerPath(self):
331 """Returns a path string that identifies the file in the SCM system.
332
333 Returns the empty string if the file does not exist in SCM.
334 """
maruel@chromium.org46a94102009-05-12 20:32:43 +0000335 return gclient.CaptureSVNInfo(self.AbsoluteLocalPath()).get('URL', '')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000336
337 def LocalPath(self):
338 """Returns the path of this file on the local disk relative to client root.
339 """
340 return normpath(self.path)
341
342 def AbsoluteLocalPath(self):
343 """Returns the absolute path of this file on the local disk.
344 """
345 return normpath(os.path.join(self.repository_root, self.LocalPath()))
346
347 def IsDirectory(self):
348 """Returns true if this object is a directory."""
maruel@chromium.org73e36ed2009-05-22 20:04:13 +0000349 path = self.AbsoluteLocalPath()
350 if os.path.exists(path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000351 # Retrieve directly from the file system; it is much faster than querying
352 # subversion, especially on Windows.
maruel@chromium.org73e36ed2009-05-22 20:04:13 +0000353 return os.path.isdir(path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000354 else:
maruel@chromium.org73e36ed2009-05-22 20:04:13 +0000355 return gclient.CaptureSVNInfo(path).get('Node Kind') in ('dir',
356 'directory')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000357
358 def SvnProperty(self, property_name):
359 """Returns the specified SVN property of this file, or the empty string
360 if no such property.
361 """
362 return gcl.GetSVNFileProperty(self.AbsoluteLocalPath(), property_name)
363
364 def Action(self):
365 """Returns the action on this opened file, e.g. A, M, D, etc."""
366 return self.action
367
368 def NewContents(self):
369 """Returns an iterator over the lines in the new version of file.
370
371 The new version is the file in the user's workspace, i.e. the "right hand
372 side".
373
374 Contents will be empty if the file is a directory or does not exist.
375 """
376 if self.IsDirectory():
377 return []
378 else:
379 return gcl.ReadFile(self.AbsoluteLocalPath()).splitlines()
380
381 def OldContents(self):
382 """Returns an iterator over the lines in the old version of file.
383
384 The old version is the file in depot, i.e. the "left hand side".
385 """
386 raise NotImplementedError() # Implement when needed
387
388 def OldFileTempPath(self):
389 """Returns the path on local disk where the old contents resides.
390
391 The old version is the file in depot, i.e. the "left hand side".
392 This is a read-only cached copy of the old contents. *DO NOT* try to
393 modify this file.
394 """
395 raise NotImplementedError() # Implement if/when needed.
396
397
398class GclChange(object):
399 """A gcl change. See gcl.ChangeInfo for more info."""
400
401 def __init__(self, change_info, repository_root=''):
402 self.name = change_info.name
403 self.full_description = change_info.description
404 self.repository_root = repository_root
405
406 # From the description text, build up a dictionary of key/value pairs
407 # plus the description minus all key/value or "tag" lines.
408 self.description_without_tags = []
409 self.tags = {}
410 for line in change_info.description.splitlines():
411 m = _tag_line_re.match(line)
412 if m:
413 self.tags[m.group('key')] = m.group('value')
414 else:
415 self.description_without_tags.append(line)
416
417 # Change back to text and remove whitespace at end.
418 self.description_without_tags = '\n'.join(self.description_without_tags)
419 self.description_without_tags = self.description_without_tags.rstrip()
420
421 self.affected_files = [AffectedFile(info[1], info[0], repository_root) for
422 info in change_info.files]
423
424 def Change(self):
425 """Returns the change name."""
426 return self.name
427
428 def Changelist(self):
429 """Synonym for Change()."""
430 return self.Change()
431
432 def DescriptionText(self):
433 """Returns the user-entered changelist description, minus tags.
434
435 Any line in the user-provided description starting with e.g. "FOO="
436 (whitespace permitted before and around) is considered a tag line. Such
437 lines are stripped out of the description this function returns.
438 """
439 return self.description_without_tags
440
441 def FullDescriptionText(self):
442 """Returns the complete changelist description including tags."""
443 return self.full_description
444
445 def RepositoryRoot(self):
446 """Returns the repository root for this change, as an absolute path."""
447 return self.repository_root
448
449 def __getattr__(self, attr):
450 """Return keys directly as attributes on the object.
451
452 You may use a friendly name (from SPECIAL_KEYS) or the actual name of
453 the key.
454 """
455 if attr in SPECIAL_KEYS:
456 key = SPECIAL_KEYS[attr]
457 if key in self.tags:
458 return self.tags[key]
459 if attr in self.tags:
460 return self.tags[attr]
461
462 def AffectedFiles(self, include_dirs=False, include_deletes=True):
463 """Returns a list of AffectedFile instances for all files in the change.
464
465 Args:
466 include_deletes: If false, deleted files will be filtered out.
467 include_dirs: True to include directories in the list
468
469 Returns:
470 [AffectedFile(path, action), AffectedFile(path, action)]
471 """
472 if include_dirs:
473 affected = self.affected_files
474 else:
475 affected = filter(lambda x: not x.IsDirectory(), self.affected_files)
476
477 if include_deletes:
478 return affected
479 else:
480 return filter(lambda x: x.Action() != 'D', affected)
481
482 def AffectedTextFiles(self, include_deletes=True):
483 """Return a list of the text files in a change.
484
485 It's common to want to iterate over only the text files.
486
487 Args:
488 include_deletes: Controls whether to return files with "delete" actions,
489 which commonly aren't relevant to presubmit scripts.
490 """
491 return InputApi.FilterTextFiles(self.AffectedFiles(include_dirs=False),
492 include_deletes)
493
494 def LocalPaths(self, include_dirs=False):
495 """Convenience function."""
496 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
497
498 def AbsoluteLocalPaths(self, include_dirs=False):
499 """Convenience function."""
500 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
501
502 def ServerPaths(self, include_dirs=False):
503 """Convenience function."""
504 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
505
506 def RightHandSideLines(self):
507 """An iterator over all text lines in "new" version of changed files.
508
509 Lists lines from new or modified text files in the change.
510
511 This is useful for doing line-by-line regex checks, like checking for
512 trailing whitespace.
513
514 Yields:
515 a 3 tuple:
516 the AffectedFile instance of the current file;
517 integer line number (1-based); and
518 the contents of the line as a string.
519 """
520 return InputApi._RightHandSideLinesImpl(
521 self.AffectedTextFiles(include_deletes=False))
522
523
524def ListRelevantPresubmitFiles(files):
525 """Finds all presubmit files that apply to a given set of source files.
526
527 Args:
528 files: An iterable container containing file paths.
529
530 Return:
531 ['foo/blat/PRESUBMIT.py', 'mat/gat/PRESUBMIT.py']
532 """
533 checked_dirs = {} # Keys are directory paths, values are ignored.
534 source_dirs = [os.path.dirname(f) for f in files]
535 presubmit_files = []
536 for dir in source_dirs:
537 while (True):
538 if dir in checked_dirs:
539 break # We've already walked up from this directory.
540
541 test_path = os.path.join(dir, 'PRESUBMIT.py')
542 if os.path.isfile(test_path):
543 presubmit_files.append(normpath(test_path))
544
545 checked_dirs[dir] = ''
546 if dir in ['', '.']:
547 break
548 else:
549 dir = os.path.dirname(dir)
550 return presubmit_files
551
552
553class PresubmitExecuter(object):
554
555 def __init__(self, change_info, committing):
556 """
557 Args:
558 change_info: The ChangeInfo object for the change.
559 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
560 """
561 self.change = GclChange(change_info, gcl.GetRepositoryRoot())
562 self.committing = committing
563
564 def ExecPresubmitScript(self, script_text, presubmit_path):
565 """Executes a single presubmit script.
566
567 Args:
568 script_text: The text of the presubmit script.
569 presubmit_path: The path to the presubmit file (this will be reported via
570 input_api.PresubmitLocalPath()).
571
572 Return:
573 A list of result objects, empty if no problems.
574 """
575 input_api = InputApi(self.change, presubmit_path)
576 context = {}
577 exec script_text in context
578
579 # These function names must change if we make substantial changes to
580 # the presubmit API that are not backwards compatible.
581 if self.committing:
582 function_name = 'CheckChangeOnCommit'
583 else:
584 function_name = 'CheckChangeOnUpload'
585 if function_name in context:
586 context['__args'] = (input_api, OutputApi())
587 result = eval(function_name + '(*__args)', context)
588 if not (isinstance(result, types.TupleType) or
589 isinstance(result, types.ListType)):
590 raise exceptions.RuntimeError(
591 'Presubmit functions must return a tuple or list')
592 for item in result:
593 if not isinstance(item, OutputApi.PresubmitResult):
594 raise exceptions.RuntimeError(
595 'All presubmit results must be of types derived from '
596 'output_api.PresubmitResult')
597 else:
598 result = () # no error since the script doesn't care about current event.
599
600 return result
601
602
603def DoPresubmitChecks(change_info,
604 committing,
605 verbose,
606 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000607 input_stream,
608 default_presubmit):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000609 """Runs all presubmit checks that apply to the files in the change.
610
611 This finds all PRESUBMIT.py files in directories enclosing the files in the
612 change (up to the repository root) and calls the relevant entrypoint function
613 depending on whether the change is being committed or uploaded.
614
615 Prints errors, warnings and notifications. Prompts the user for warnings
616 when needed.
617
618 Args:
619 change_info: The ChangeInfo object for the change.
620 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
621 verbose: Prints debug info.
622 output_stream: A stream to write output from presubmit tests to.
623 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000624 default_presubmit: A default presubmit script to execute in any case.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000625
626 Return:
627 True if execution can continue, False if not.
628 """
629 presubmit_files = ListRelevantPresubmitFiles(change_info.FileList())
630 if not presubmit_files and verbose:
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000631 output_stream.write("Warning, no presubmit.py found.")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000632 results = []
633 executer = PresubmitExecuter(change_info, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000634 if default_presubmit:
635 if verbose:
636 output_stream.write("Running default presubmit script")
637 results += executer.ExecPresubmitScript(default_presubmit, 'PRESUBMIT.py')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000638 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +0000639 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000640 if verbose:
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000641 output_stream.write("Running %s" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +0000642 # Accept CRLF presubmit script.
maruel@chromium.org277003e2009-05-01 12:51:43 +0000643 presubmit_script = gcl.ReadFile(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000644 results += executer.ExecPresubmitScript(presubmit_script, filename)
645
646 errors = []
647 notifications = []
648 warnings = []
649 for result in results:
650 if not result.IsFatal() and not result.ShouldPrompt():
651 notifications.append(result)
652 elif result.ShouldPrompt():
653 warnings.append(result)
654 else:
655 errors.append(result)
656
657 error_count = 0
658 for name, items in (('Messages', notifications),
659 ('Warnings', warnings),
660 ('ERRORS', errors)):
661 if items:
662 output_stream.write('\n** Presubmit %s **\n\n' % name)
663 for item in items:
664 if not item._Handle(output_stream, input_stream,
665 may_prompt=False):
666 error_count += 1
667 output_stream.write('\n')
668 if not errors and warnings:
669 output_stream.write(
670 'There were presubmit warnings. Sure you want to continue? (y/N): ')
671 response = input_stream.readline()
672 if response.strip().lower() != 'y':
673 error_count += 1
674 return (error_count == 0)
675
676
677def ScanSubDirs(mask, recursive):
678 if not recursive:
679 return [x for x in glob.glob(mask) if '.svn' not in x]
680 else:
681 results = []
682 for root, dirs, files in os.walk('.'):
683 if '.svn' in dirs:
684 dirs.remove('.svn')
685 for name in files:
686 if fnmatch.fnmatch(name, mask):
687 results.append(os.path.join(root, name))
688 return results
689
690
691def ParseFiles(args, recursive):
692 files = []
693 for arg in args:
694 files.extend([('M', file) for file in ScanSubDirs(arg, recursive)])
695 return files
696
697
698def Main(argv):
699 parser = optparse.OptionParser(usage="%prog [options]",
700 version="%prog " + str(__version__))
701 parser.add_option("-c", "--commit", action="store_true",
702 help="Use commit instead of upload checks")
703 parser.add_option("-r", "--recursive", action="store_true",
704 help="Act recursively")
705 parser.add_option("-v", "--verbose", action="store_true",
706 help="Verbose output")
707 options, args = parser.parse_args(argv[1:])
708 files = ParseFiles(args, options.recursive)
709 if options.verbose:
710 print "Found %d files." % len(files)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000711 return not DoPresubmitChecks(gcl.ChangeInfo(name='temp', files=files),
712 options.commit,
713 options.verbose,
714 sys.stdout,
715 sys.stdin,
716 default_presubmit=None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717
718
719if __name__ == '__main__':
720 sys.exit(Main(sys.argv))