OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Enables directory-specific presubmit checks to run at upload and/or commit. | 6 """Enables directory-specific presubmit checks to run at upload and/or commit. |
7 """ | 7 """ |
8 | 8 |
9 __version__ = '1.3.5' | 9 __version__ = '1.3.5' |
10 | 10 |
11 # TODO(joi) Add caching where appropriate/needed. The API is designed to allow | 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 | 12 # caching (between all different invocations of presubmit scripts for a given |
(...skipping 12 matching lines...) Expand all Loading... |
25 import random | 25 import random |
26 import re # Exposed through the API. | 26 import re # Exposed through the API. |
27 import subprocess # Exposed through the API. | 27 import subprocess # Exposed through the API. |
28 import sys # Parts exposed through API. | 28 import sys # Parts exposed through API. |
29 import tempfile # Exposed through the API. | 29 import tempfile # Exposed through the API. |
30 import time | 30 import time |
31 import traceback # Exposed through the API. | 31 import traceback # Exposed through the API. |
32 import types | 32 import types |
33 import unittest # Exposed through the API. | 33 import unittest # Exposed through the API. |
34 import urllib2 # Exposed through the API. | 34 import urllib2 # Exposed through the API. |
35 import warnings | 35 from warnings import warn |
36 | 36 |
37 try: | 37 try: |
38 import simplejson as json | 38 import simplejson as json |
39 except ImportError: | 39 except ImportError: |
40 try: | 40 try: |
41 import json | 41 import json |
42 # Some versions of python2.5 have an incomplete json module. Check to make | 42 # Some versions of python2.5 have an incomplete json module. Check to make |
43 # sure loads exists. | 43 # sure loads exists. |
44 json.loads | 44 json.loads |
45 except (ImportError, AttributeError): | 45 except (ImportError, AttributeError): |
46 # Import the one included in depot_tools. | 46 # Import the one included in depot_tools. |
47 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party')) | 47 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party')) |
48 import simplejson as json | 48 import simplejson as json |
49 | 49 |
50 # Local imports. | 50 # Local imports. |
51 import gcl | |
52 import gclient_utils | 51 import gclient_utils |
53 import presubmit_canned_checks | 52 import presubmit_canned_checks |
54 import scm | 53 import scm |
55 | 54 |
56 | 55 |
57 # Ask for feedback only once in program lifetime. | 56 # Ask for feedback only once in program lifetime. |
58 _ASKED_FOR_FEEDBACK = False | 57 _ASKED_FOR_FEEDBACK = False |
59 | 58 |
60 | 59 |
61 class NotImplementedException(Exception): | 60 class NotImplementedException(Exception): |
62 """We're leaving placeholders in a bunch of places to remind us of the | 61 """We're leaving placeholders in a bunch of places to remind us of the |
63 design of the API, but we have not implemented all of it yet. Implement as | 62 design of the API, but we have not implemented all of it yet. Implement as |
64 the need arises. | 63 the need arises. |
65 """ | 64 """ |
66 pass | 65 pass |
67 | 66 |
68 | 67 |
69 def normpath(path): | 68 def normpath(path): |
70 '''Version of os.path.normpath that also changes backward slashes to | 69 '''Version of os.path.normpath that also changes backward slashes to |
71 forward slashes when not running on Windows. | 70 forward slashes when not running on Windows. |
72 ''' | 71 ''' |
73 # This is safe to always do because the Windows version of os.path.normpath | 72 # This is safe to always do because the Windows version of os.path.normpath |
74 # will replace forward slashes with backward slashes. | 73 # will replace forward slashes with backward slashes. |
75 path = path.replace(os.sep, '/') | 74 path = path.replace(os.sep, '/') |
76 return os.path.normpath(path) | 75 return os.path.normpath(path) |
77 | 76 |
| 77 |
78 def PromptYesNo(input_stream, output_stream, prompt): | 78 def PromptYesNo(input_stream, output_stream, prompt): |
79 output_stream.write(prompt) | 79 output_stream.write(prompt) |
80 response = input_stream.readline().strip().lower() | 80 response = input_stream.readline().strip().lower() |
81 return response == 'y' or response == 'yes' | 81 return response == 'y' or response == 'yes' |
82 | 82 |
| 83 |
| 84 def _RightHandSideLinesImpl(affected_files): |
| 85 """Implements RightHandSideLines for InputApi and GclChange.""" |
| 86 for af in affected_files: |
| 87 lines = af.NewContents() |
| 88 line_number = 0 |
| 89 for line in lines: |
| 90 line_number += 1 |
| 91 yield (af, line_number, line) |
| 92 |
| 93 |
83 class OutputApi(object): | 94 class OutputApi(object): |
84 """This class (more like a module) gets passed to presubmit scripts so that | 95 """This class (more like a module) gets passed to presubmit scripts so that |
85 they can specify various types of results. | 96 they can specify various types of results. |
86 """ | 97 """ |
87 | 98 |
88 class PresubmitResult(object): | 99 class PresubmitResult(object): |
89 """Base class for result objects.""" | 100 """Base class for result objects.""" |
90 | 101 |
91 def __init__(self, message, items=None, long_text=''): | 102 def __init__(self, message, items=None, long_text=''): |
92 """ | 103 """ |
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
151 def ShouldPrompt(self): | 162 def ShouldPrompt(self): |
152 return True | 163 return True |
153 | 164 |
154 class PresubmitNotifyResult(PresubmitResult): | 165 class PresubmitNotifyResult(PresubmitResult): |
155 """Just print something to the screen -- but it's not even a warning.""" | 166 """Just print something to the screen -- but it's not even a warning.""" |
156 pass | 167 pass |
157 | 168 |
158 class MailTextResult(PresubmitResult): | 169 class MailTextResult(PresubmitResult): |
159 """A warning that should be included in the review request email.""" | 170 """A warning that should be included in the review request email.""" |
160 def __init__(self, *args, **kwargs): | 171 def __init__(self, *args, **kwargs): |
161 raise NotImplementedException() # TODO(joi) Implement. | 172 super(OutputApi.MailTextResult, self).__init__() |
| 173 raise NotImplementedException() |
162 | 174 |
163 | 175 |
164 class InputApi(object): | 176 class InputApi(object): |
165 """An instance of this object is passed to presubmit scripts so they can | 177 """An instance of this object is passed to presubmit scripts so they can |
166 know stuff about the change they're looking at. | 178 know stuff about the change they're looking at. |
167 """ | 179 """ |
168 | 180 |
169 # File extensions that are considered source files from a style guide | 181 # File extensions that are considered source files from a style guide |
170 # perspective. Don't modify this list from a presubmit script! | 182 # perspective. Don't modify this list from a presubmit script! |
171 DEFAULT_WHITE_LIST = ( | 183 DEFAULT_WHITE_LIST = ( |
(...skipping 130 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
302 def ServerPaths(self, include_dirs=False): | 314 def ServerPaths(self, include_dirs=False): |
303 """Returns server paths of input_api.AffectedFiles().""" | 315 """Returns server paths of input_api.AffectedFiles().""" |
304 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)] | 316 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)] |
305 | 317 |
306 def AffectedTextFiles(self, include_deletes=None): | 318 def AffectedTextFiles(self, include_deletes=None): |
307 """Same as input_api.change.AffectedTextFiles() except only lists files | 319 """Same as input_api.change.AffectedTextFiles() except only lists files |
308 in the same directory as the current presubmit script, or subdirectories | 320 in the same directory as the current presubmit script, or subdirectories |
309 thereof. | 321 thereof. |
310 """ | 322 """ |
311 if include_deletes is not None: | 323 if include_deletes is not None: |
312 warnings.warn("AffectedTextFiles(include_deletes=%s)" | 324 warn("AffectedTextFiles(include_deletes=%s)" |
313 " is deprecated and ignored" % str(include_deletes), | 325 " is deprecated and ignored" % str(include_deletes), |
314 category=DeprecationWarning, | 326 category=DeprecationWarning, |
315 stacklevel=2) | 327 stacklevel=2) |
316 return filter(lambda x: x.IsTextFile(), | 328 return filter(lambda x: x.IsTextFile(), |
317 self.AffectedFiles(include_dirs=False, include_deletes=False)) | 329 self.AffectedFiles(include_dirs=False, include_deletes=False)) |
318 | 330 |
319 def FilterSourceFile(self, affected_file, white_list=None, black_list=None): | 331 def FilterSourceFile(self, affected_file, white_list=None, black_list=None): |
320 """Filters out files that aren't considered "source file". | 332 """Filters out files that aren't considered "source file". |
321 | 333 |
322 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST | 334 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST |
323 and InputApi.DEFAULT_BLACK_LIST is used respectively. | 335 and InputApi.DEFAULT_BLACK_LIST is used respectively. |
324 | 336 |
325 The lists will be compiled as regular expression and | 337 The lists will be compiled as regular expression and |
326 AffectedFile.LocalPath() needs to pass both list. | 338 AffectedFile.LocalPath() needs to pass both list. |
327 | 339 |
328 Note: Copy-paste this function to suit your needs or use a lambda function. | 340 Note: Copy-paste this function to suit your needs or use a lambda function. |
329 """ | 341 """ |
330 def Find(affected_file, list): | 342 def Find(affected_file, items): |
331 for item in list: | 343 for item in items: |
332 local_path = affected_file.LocalPath() | 344 local_path = affected_file.LocalPath() |
333 if self.re.match(item, local_path): | 345 if self.re.match(item, local_path): |
334 logging.debug("%s matched %s" % (item, local_path)) | 346 logging.debug("%s matched %s" % (item, local_path)) |
335 return True | 347 return True |
336 return False | 348 return False |
337 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and | 349 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and |
338 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST)) | 350 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST)) |
339 | 351 |
340 def AffectedSourceFiles(self, source_file): | 352 def AffectedSourceFiles(self, source_file): |
341 """Filter the list of AffectedTextFiles by the function source_file. | 353 """Filter the list of AffectedTextFiles by the function source_file. |
(...skipping 15 matching lines...) Expand all Loading... |
357 | 369 |
358 Yields: | 370 Yields: |
359 a 3 tuple: | 371 a 3 tuple: |
360 the AffectedFile instance of the current file; | 372 the AffectedFile instance of the current file; |
361 integer line number (1-based); and | 373 integer line number (1-based); and |
362 the contents of the line as a string. | 374 the contents of the line as a string. |
363 | 375 |
364 Note: The cariage return (LF or CR) is stripped off. | 376 Note: The cariage return (LF or CR) is stripped off. |
365 """ | 377 """ |
366 files = self.AffectedSourceFiles(source_file_filter) | 378 files = self.AffectedSourceFiles(source_file_filter) |
367 return InputApi._RightHandSideLinesImpl(files) | 379 return _RightHandSideLinesImpl(files) |
368 | 380 |
369 def ReadFile(self, file_item, mode='r'): | 381 def ReadFile(self, file_item, mode='r'): |
370 """Reads an arbitrary file. | 382 """Reads an arbitrary file. |
371 | 383 |
372 Deny reading anything outside the repository. | 384 Deny reading anything outside the repository. |
373 """ | 385 """ |
374 if isinstance(file_item, AffectedFile): | 386 if isinstance(file_item, AffectedFile): |
375 file_item = file_item.AbsoluteLocalPath() | 387 file_item = file_item.AbsoluteLocalPath() |
376 if not file_item.startswith(self.change.RepositoryRoot()): | 388 if not file_item.startswith(self.change.RepositoryRoot()): |
377 raise IOError('Access outside the repository root is denied.') | 389 raise IOError('Access outside the repository root is denied.') |
378 return gclient_utils.FileRead(file_item, mode) | 390 return gclient_utils.FileRead(file_item, mode) |
379 | 391 |
380 @staticmethod | |
381 def _RightHandSideLinesImpl(affected_files): | |
382 """Implements RightHandSideLines for InputApi and GclChange.""" | |
383 for af in affected_files: | |
384 lines = af.NewContents() | |
385 line_number = 0 | |
386 for line in lines: | |
387 line_number += 1 | |
388 yield (af, line_number, line) | |
389 | |
390 | 392 |
391 class AffectedFile(object): | 393 class AffectedFile(object): |
392 """Representation of a file in a change.""" | 394 """Representation of a file in a change.""" |
393 | 395 |
394 def __init__(self, path, action, repository_root=''): | 396 def __init__(self, path, action, repository_root=''): |
395 self._path = path | 397 self._path = path |
396 self._action = action | 398 self._action = action |
397 self._local_root = repository_root | 399 self._local_root = repository_root |
398 self._is_directory = None | 400 self._is_directory = None |
399 self._properties = {} | 401 self._properties = {} |
(...skipping 258 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
658 affected = filter(lambda x: not x.IsDirectory(), self._affected_files) | 660 affected = filter(lambda x: not x.IsDirectory(), self._affected_files) |
659 | 661 |
660 if include_deletes: | 662 if include_deletes: |
661 return affected | 663 return affected |
662 else: | 664 else: |
663 return filter(lambda x: x.Action() != 'D', affected) | 665 return filter(lambda x: x.Action() != 'D', affected) |
664 | 666 |
665 def AffectedTextFiles(self, include_deletes=None): | 667 def AffectedTextFiles(self, include_deletes=None): |
666 """Return a list of the existing text files in a change.""" | 668 """Return a list of the existing text files in a change.""" |
667 if include_deletes is not None: | 669 if include_deletes is not None: |
668 warnings.warn("AffectedTextFiles(include_deletes=%s)" | 670 warn("AffectedTextFiles(include_deletes=%s)" |
669 " is deprecated and ignored" % str(include_deletes), | 671 " is deprecated and ignored" % str(include_deletes), |
670 category=DeprecationWarning, | 672 category=DeprecationWarning, |
671 stacklevel=2) | 673 stacklevel=2) |
672 return filter(lambda x: x.IsTextFile(), | 674 return filter(lambda x: x.IsTextFile(), |
673 self.AffectedFiles(include_dirs=False, include_deletes=False)) | 675 self.AffectedFiles(include_dirs=False, include_deletes=False)) |
674 | 676 |
675 def LocalPaths(self, include_dirs=False): | 677 def LocalPaths(self, include_dirs=False): |
676 """Convenience function.""" | 678 """Convenience function.""" |
677 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)] | 679 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)] |
678 | 680 |
679 def AbsoluteLocalPaths(self, include_dirs=False): | 681 def AbsoluteLocalPaths(self, include_dirs=False): |
680 """Convenience function.""" | 682 """Convenience function.""" |
681 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)] | 683 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)] |
682 | 684 |
683 def ServerPaths(self, include_dirs=False): | 685 def ServerPaths(self, include_dirs=False): |
684 """Convenience function.""" | 686 """Convenience function.""" |
685 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)] | 687 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)] |
686 | 688 |
687 def RightHandSideLines(self): | 689 def RightHandSideLines(self): |
688 """An iterator over all text lines in "new" version of changed files. | 690 """An iterator over all text lines in "new" version of changed files. |
689 | 691 |
690 Lists lines from new or modified text files in the change. | 692 Lists lines from new or modified text files in the change. |
691 | 693 |
692 This is useful for doing line-by-line regex checks, like checking for | 694 This is useful for doing line-by-line regex checks, like checking for |
693 trailing whitespace. | 695 trailing whitespace. |
694 | 696 |
695 Yields: | 697 Yields: |
696 a 3 tuple: | 698 a 3 tuple: |
697 the AffectedFile instance of the current file; | 699 the AffectedFile instance of the current file; |
698 integer line number (1-based); and | 700 integer line number (1-based); and |
699 the contents of the line as a string. | 701 the contents of the line as a string. |
700 """ | 702 """ |
701 return InputApi._RightHandSideLinesImpl( | 703 return _RightHandSideLinesImpl( |
702 filter(lambda x: x.IsTextFile(), | 704 x for x in self.AffectedFiles(include_deletes=False) |
703 self.AffectedFiles(include_deletes=False))) | 705 if x.IsTextFile()) |
704 | 706 |
705 | 707 |
706 class SvnChange(Change): | 708 class SvnChange(Change): |
707 _AFFECTED_FILES = SvnAffectedFile | 709 _AFFECTED_FILES = SvnAffectedFile |
708 | 710 |
709 def __init__(self, *args, **kwargs): | 711 def __init__(self, *args, **kwargs): |
710 Change.__init__(self, *args, **kwargs) | 712 Change.__init__(self, *args, **kwargs) |
711 self.scm = 'svn' | 713 self.scm = 'svn' |
712 self._changelists = None | 714 self._changelists = None |
713 | 715 |
714 def _GetChangeLists(self): | 716 def _GetChangeLists(self): |
715 """Get all change lists.""" | 717 """Get all change lists.""" |
716 if self._changelists == None: | 718 if self._changelists == None: |
717 previous_cwd = os.getcwd() | 719 previous_cwd = os.getcwd() |
718 os.chdir(self.RepositoryRoot()) | 720 os.chdir(self.RepositoryRoot()) |
| 721 # Need to import here to avoid circular dependency. |
| 722 import gcl |
719 self._changelists = gcl.GetModifiedFiles() | 723 self._changelists = gcl.GetModifiedFiles() |
720 os.chdir(previous_cwd) | 724 os.chdir(previous_cwd) |
721 return self._changelists | 725 return self._changelists |
722 | 726 |
723 def GetAllModifiedFiles(self): | 727 def GetAllModifiedFiles(self): |
724 """Get all modified files.""" | 728 """Get all modified files.""" |
725 changelists = self._GetChangeLists() | 729 changelists = self._GetChangeLists() |
726 all_modified_files = [] | 730 all_modified_files = [] |
727 for cl in changelists.values(): | 731 for cl in changelists.values(): |
728 all_modified_files.extend( | 732 all_modified_files.extend( |
(...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
785 results = [] | 789 results = [] |
786 for directory in sorted(list(candidates)): | 790 for directory in sorted(list(candidates)): |
787 p = os.path.join(directory, 'PRESUBMIT.py') | 791 p = os.path.join(directory, 'PRESUBMIT.py') |
788 if os.path.isfile(p): | 792 if os.path.isfile(p): |
789 results.append(p) | 793 results.append(p) |
790 | 794 |
791 return results | 795 return results |
792 | 796 |
793 | 797 |
794 class GetTrySlavesExecuter(object): | 798 class GetTrySlavesExecuter(object): |
795 def ExecPresubmitScript(self, script_text): | 799 @staticmethod |
| 800 def ExecPresubmitScript(script_text): |
796 """Executes GetPreferredTrySlaves() from a single presubmit script. | 801 """Executes GetPreferredTrySlaves() from a single presubmit script. |
797 | 802 |
798 Args: | 803 Args: |
799 script_text: The text of the presubmit script. | 804 script_text: The text of the presubmit script. |
800 | 805 |
801 Return: | 806 Return: |
802 A list of try slaves. | 807 A list of try slaves. |
803 """ | 808 """ |
804 context = {} | 809 context = {} |
805 exec script_text in context | 810 exec script_text in context |
(...skipping 289 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1095 options.commit, | 1100 options.commit, |
1096 options.verbose, | 1101 options.verbose, |
1097 sys.stdout, | 1102 sys.stdout, |
1098 sys.stdin, | 1103 sys.stdin, |
1099 options.default_presubmit, | 1104 options.default_presubmit, |
1100 options.may_prompt) | 1105 options.may_prompt) |
1101 | 1106 |
1102 | 1107 |
1103 if __name__ == '__main__': | 1108 if __name__ == '__main__': |
1104 sys.exit(Main(sys.argv)) | 1109 sys.exit(Main(sys.argv)) |
OLD | NEW |