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