OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 # Copyright (c) 2010 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 |
(...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
79 | 79 |
80 def PromptYesNo(input_stream, output_stream, prompt): | 80 def PromptYesNo(input_stream, output_stream, prompt): |
81 output_stream.write(prompt) | 81 output_stream.write(prompt) |
82 response = input_stream.readline().strip().lower() | 82 response = input_stream.readline().strip().lower() |
83 return response == 'y' or response == 'yes' | 83 return response == 'y' or response == 'yes' |
84 | 84 |
85 | 85 |
86 def _RightHandSideLinesImpl(affected_files): | 86 def _RightHandSideLinesImpl(affected_files): |
87 """Implements RightHandSideLines for InputApi and GclChange.""" | 87 """Implements RightHandSideLines for InputApi and GclChange.""" |
88 for af in affected_files: | 88 for af in affected_files: |
89 lines = af.NewContents() | 89 lines = af.ChangedContents() |
90 line_number = 0 | |
91 for line in lines: | 90 for line in lines: |
92 line_number += 1 | 91 yield (af, line[0], line[1]) |
93 yield (af, line_number, line) | |
94 | 92 |
95 | 93 |
96 class OutputApi(object): | 94 class OutputApi(object): |
97 """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 |
98 they can specify various types of results. | 96 they can specify various types of results. |
99 """ | 97 """ |
100 # Method could be a function | 98 # Method could be a function |
101 # pylint: disable=R0201 | 99 # pylint: disable=R0201 |
102 class PresubmitResult(object): | 100 class PresubmitResult(object): |
103 """Base class for result objects.""" | 101 """Base class for result objects.""" |
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
181 know stuff about the change they're looking at. | 179 know stuff about the change they're looking at. |
182 """ | 180 """ |
183 # Method could be a function | 181 # Method could be a function |
184 # pylint: disable=R0201 | 182 # pylint: disable=R0201 |
185 | 183 |
186 # File extensions that are considered source files from a style guide | 184 # File extensions that are considered source files from a style guide |
187 # perspective. Don't modify this list from a presubmit script! | 185 # perspective. Don't modify this list from a presubmit script! |
188 DEFAULT_WHITE_LIST = ( | 186 DEFAULT_WHITE_LIST = ( |
189 # C++ and friends | 187 # C++ and friends |
190 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$", | 188 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$", |
191 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", | 189 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$", |
192 # Scripts | 190 # Scripts |
193 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$", | 191 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$", |
194 # No extension at all, note that ALL CAPS files are black listed in | 192 # No extension at all, note that ALL CAPS files are black listed in |
195 # DEFAULT_BLACK_LIST below. | 193 # DEFAULT_BLACK_LIST below. |
196 r"(^|.*?[\\\/])[^.]+$", | 194 r"(^|.*?[\\\/])[^.]+$", |
197 # Other | 195 # Other |
198 r".*\.java$", r".*\.mk$", r".*\.am$", | 196 r".*\.java$", r".*\.mk$", r".*\.am$", |
199 ) | 197 ) |
200 | 198 |
201 # Path regexp that should be excluded from being considered containing source | 199 # Path regexp that should be excluded from being considered containing source |
202 # files. Don't modify this list from a presubmit script! | 200 # files. Don't modify this list from a presubmit script! |
203 DEFAULT_BLACK_LIST = ( | 201 DEFAULT_BLACK_LIST = ( |
204 r".*\bexperimental[\\\/].*", | 202 r".*\bexperimental[\\\/].*", |
205 r".*\bthird_party[\\\/].*", | 203 r".*\bthird_party[\\\/].*", |
206 # Output directories (just in case) | 204 # Output directories (just in case) |
207 r".*\bDebug[\\\/].*", | 205 r".*\bDebug[\\\/].*", |
208 r".*\bRelease[\\\/].*", | 206 r".*\bRelease[\\\/].*", |
209 r".*\bxcodebuild[\\\/].*", | 207 r".*\bxcodebuild[\\\/].*", |
210 r".*\bsconsbuild[\\\/].*", | 208 r".*\bsconsbuild[\\\/].*", |
211 # All caps files like README and LICENCE. | 209 # All caps files like README and LICENCE. |
212 r".*\b[A-Z0-9_]+$", | 210 r".*\b[A-Z0-9_]{2,}$", |
213 # SCM (can happen in dual SCM configuration). (Slightly over aggressive) | 211 # SCM (can happen in dual SCM configuration). (Slightly over aggressive) |
214 r"(|.*[\\\/])\.git[\\\/].*", | 212 r"(|.*[\\\/])\.git[\\\/].*", |
215 r"(|.*[\\\/])\.svn[\\\/].*", | 213 r"(|.*[\\\/])\.svn[\\\/].*", |
216 ) | 214 ) |
217 | 215 |
218 def __init__(self, change, presubmit_path, is_committing): | 216 def __init__(self, change, presubmit_path, is_committing): |
219 """Builds an InputApi object. | 217 """Builds an InputApi object. |
220 | 218 |
221 Args: | 219 Args: |
222 change: A presubmit.Change object. | 220 change: A presubmit.Change object. |
(...skipping 115 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
338 | 336 |
339 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST | 337 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST |
340 and InputApi.DEFAULT_BLACK_LIST is used respectively. | 338 and InputApi.DEFAULT_BLACK_LIST is used respectively. |
341 | 339 |
342 The lists will be compiled as regular expression and | 340 The lists will be compiled as regular expression and |
343 AffectedFile.LocalPath() needs to pass both list. | 341 AffectedFile.LocalPath() needs to pass both list. |
344 | 342 |
345 Note: Copy-paste this function to suit your needs or use a lambda function. | 343 Note: Copy-paste this function to suit your needs or use a lambda function. |
346 """ | 344 """ |
347 def Find(affected_file, items): | 345 def Find(affected_file, items): |
| 346 local_path = affected_file.LocalPath() |
348 for item in items: | 347 for item in items: |
349 local_path = affected_file.LocalPath() | |
350 if self.re.match(item, local_path): | 348 if self.re.match(item, local_path): |
351 logging.debug("%s matched %s" % (item, local_path)) | 349 logging.debug("%s matched %s" % (item, local_path)) |
352 return True | 350 return True |
353 return False | 351 return False |
354 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and | 352 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and |
355 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST)) | 353 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST)) |
356 | 354 |
357 def AffectedSourceFiles(self, source_file): | 355 def AffectedSourceFiles(self, source_file): |
358 """Filter the list of AffectedTextFiles by the function source_file. | 356 """Filter the list of AffectedTextFiles by the function source_file. |
359 | 357 |
(...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
474 | 472 |
475 def OldFileTempPath(self): | 473 def OldFileTempPath(self): |
476 """Returns the path on local disk where the old contents resides. | 474 """Returns the path on local disk where the old contents resides. |
477 | 475 |
478 The old version is the file in depot, i.e. the "left hand side". | 476 The old version is the file in depot, i.e. the "left hand side". |
479 This is a read-only cached copy of the old contents. *DO NOT* try to | 477 This is a read-only cached copy of the old contents. *DO NOT* try to |
480 modify this file. | 478 modify this file. |
481 """ | 479 """ |
482 raise NotImplementedError() # Implement if/when needed. | 480 raise NotImplementedError() # Implement if/when needed. |
483 | 481 |
| 482 def ChangedContents(self): |
| 483 """Returns a list of tuples (line number, line text) of all new lines. |
| 484 |
| 485 This relies on the scm diff output describing each changed code section |
| 486 with a line of the form |
| 487 |
| 488 ^@@ <old line num>,<old size> <new line num>,<new size> @@$ |
| 489 """ |
| 490 new_lines = [] |
| 491 line_num = 0 |
| 492 |
| 493 if self.IsDirectory(): |
| 494 return [] |
| 495 |
| 496 for line in self.GenerateScmDiff().splitlines(): |
| 497 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line) |
| 498 if m: |
| 499 line_num = int(m.groups(1)[0]) |
| 500 continue |
| 501 if line.startswith('+') and not line.startswith('++'): |
| 502 new_lines.append((line_num, line[1:])) |
| 503 if not line.startswith('-'): |
| 504 line_num += 1 |
| 505 return new_lines |
| 506 |
484 def __str__(self): | 507 def __str__(self): |
485 return self.LocalPath() | 508 return self.LocalPath() |
486 | 509 |
| 510 def GenerateScmDiff(self): |
| 511 raise NotImplementedError() # Implemented in derived classes. |
487 | 512 |
488 class SvnAffectedFile(AffectedFile): | 513 class SvnAffectedFile(AffectedFile): |
489 """Representation of a file in a change out of a Subversion checkout.""" | 514 """Representation of a file in a change out of a Subversion checkout.""" |
490 # Method 'NNN' is abstract in class 'NNN' but is not overridden | 515 # Method 'NNN' is abstract in class 'NNN' but is not overridden |
491 # pylint: disable=W0223 | 516 # pylint: disable=W0223 |
492 | 517 |
493 def __init__(self, *args, **kwargs): | 518 def __init__(self, *args, **kwargs): |
494 AffectedFile.__init__(self, *args, **kwargs) | 519 AffectedFile.__init__(self, *args, **kwargs) |
495 self._server_path = None | 520 self._server_path = None |
496 self._is_text_file = None | 521 self._is_text_file = None |
(...skipping 28 matching lines...) Expand all Loading... |
525 # A deleted file is not a text file. | 550 # A deleted file is not a text file. |
526 self._is_text_file = False | 551 self._is_text_file = False |
527 elif self.IsDirectory(): | 552 elif self.IsDirectory(): |
528 self._is_text_file = False | 553 self._is_text_file = False |
529 else: | 554 else: |
530 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(), | 555 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(), |
531 'svn:mime-type') | 556 'svn:mime-type') |
532 self._is_text_file = (not mime_type or mime_type.startswith('text/')) | 557 self._is_text_file = (not mime_type or mime_type.startswith('text/')) |
533 return self._is_text_file | 558 return self._is_text_file |
534 | 559 |
| 560 def GenerateScmDiff(self): |
| 561 return scm.SVN.GenerateDiff(self.AbsoluteLocalPath()) |
535 | 562 |
536 class GitAffectedFile(AffectedFile): | 563 class GitAffectedFile(AffectedFile): |
537 """Representation of a file in a change out of a git checkout.""" | 564 """Representation of a file in a change out of a git checkout.""" |
538 # Method 'NNN' is abstract in class 'NNN' but is not overridden | 565 # Method 'NNN' is abstract in class 'NNN' but is not overridden |
539 # pylint: disable=W0223 | 566 # pylint: disable=W0223 |
540 | 567 |
541 def __init__(self, *args, **kwargs): | 568 def __init__(self, *args, **kwargs): |
542 AffectedFile.__init__(self, *args, **kwargs) | 569 AffectedFile.__init__(self, *args, **kwargs) |
543 self._server_path = None | 570 self._server_path = None |
544 self._is_text_file = None | 571 self._is_text_file = None |
(...skipping 25 matching lines...) Expand all Loading... |
570 if self.Action() == 'D': | 597 if self.Action() == 'D': |
571 # A deleted file is not a text file. | 598 # A deleted file is not a text file. |
572 self._is_text_file = False | 599 self._is_text_file = False |
573 elif self.IsDirectory(): | 600 elif self.IsDirectory(): |
574 self._is_text_file = False | 601 self._is_text_file = False |
575 else: | 602 else: |
576 # raise NotImplementedException() # TODO(maruel) Implement. | 603 # raise NotImplementedException() # TODO(maruel) Implement. |
577 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath()) | 604 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath()) |
578 return self._is_text_file | 605 return self._is_text_file |
579 | 606 |
| 607 def GenerateScmDiff(self): |
| 608 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),]) |
580 | 609 |
581 class Change(object): | 610 class Change(object): |
582 """Describe a change. | 611 """Describe a change. |
583 | 612 |
584 Used directly by the presubmit scripts to query the current change being | 613 Used directly by the presubmit scripts to query the current change being |
585 tested. | 614 tested. |
586 | 615 |
587 Instance members: | 616 Instance members: |
588 tags: Dictionnary of KEY=VALUE pairs found in the change description. | 617 tags: Dictionnary of KEY=VALUE pairs found in the change description. |
589 self.KEY: equivalent to tags['KEY'] | 618 self.KEY: equivalent to tags['KEY'] |
(...skipping 538 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1128 options.commit, | 1157 options.commit, |
1129 options.verbose, | 1158 options.verbose, |
1130 sys.stdout, | 1159 sys.stdout, |
1131 sys.stdin, | 1160 sys.stdin, |
1132 options.default_presubmit, | 1161 options.default_presubmit, |
1133 options.may_prompt) | 1162 options.may_prompt) |
1134 | 1163 |
1135 | 1164 |
1136 if __name__ == '__main__': | 1165 if __name__ == '__main__': |
1137 sys.exit(Main(sys.argv)) | 1166 sys.exit(Main(sys.argv)) |
OLD | NEW |