| 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 |