Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 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.6.2' | 9 __version__ = '1.6.2' |
| 10 | 10 |
| (...skipping 471 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 482 pool = multiprocessing.Pool() | 482 pool = multiprocessing.Pool() |
| 483 # async recipe works around multiprocessing bug handling Ctrl-C | 483 # async recipe works around multiprocessing bug handling Ctrl-C |
| 484 msgs.extend(pool.map_async(CallCommand, tests).get(99999)) | 484 msgs.extend(pool.map_async(CallCommand, tests).get(99999)) |
| 485 pool.close() | 485 pool.close() |
| 486 pool.join() | 486 pool.join() |
| 487 else: | 487 else: |
| 488 msgs.extend(map(CallCommand, tests)) | 488 msgs.extend(map(CallCommand, tests)) |
| 489 return [m for m in msgs if m] | 489 return [m for m in msgs if m] |
| 490 | 490 |
| 491 | 491 |
| 492 class _DiffCache(object): | |
|
iannucci
2013/06/07 02:17:36
Why not build this into the scm interface and hide
ncarter (slow)
2013/06/07 19:51:15
It seemed risky, since there are many other caller
iannucci
2013/06/07 21:30:16
Hmm.. fair point. This code is pretty labyrinthine
| |
| 493 """Caches diffs retrieved from a particular SCM.""" | |
| 494 | |
| 495 def GetScmDiff(self, path, local_root): | |
|
iannucci
2013/06/07 02:17:36
Maybe 'GetDiff'?
ncarter (slow)
2013/06/07 19:51:15
Done.
| |
| 496 """Get the diff for a particular path.""" | |
| 497 raise NotImplementedError() | |
| 498 | |
| 499 | |
| 500 class _SvnDiffCache(_DiffCache): | |
| 501 """DiffCache implementation for subversion.""" | |
| 502 def __init__(self): | |
| 503 _DiffCache.__init__(self) | |
|
iannucci
2013/06/07 02:17:36
super(_SvnDiffCache, self).__init__()
ncarter (slow)
2013/06/07 19:51:15
D'oh, how rusty of me. I knew this looked funny bu
| |
| 504 self._diffs_by_file = {} | |
| 505 | |
| 506 def GetScmDiff(self, path, local_root): | |
| 507 if path not in self._diffs_by_file: | |
| 508 self._diffs_by_file[path] = scm.SVN.GenerateDiff([path], local_root, | |
| 509 False, None) | |
| 510 return self._diffs_by_file[path] | |
| 511 | |
| 512 | |
| 513 class _GitDiffCache(_DiffCache): | |
| 514 """DiffCache implementation for git; gets all file diffs at once.""" | |
| 515 def __init__(self): | |
| 516 _DiffCache.__init__(self) | |
|
iannucci
2013/06/07 02:17:36
super(_GitDiffCache, self).__init__()
ncarter (slow)
2013/06/07 19:51:15
Done.
| |
| 517 self._diffs_by_file = None | |
| 518 | |
| 519 def GetScmDiff(self, path, local_root): | |
| 520 if not self._diffs_by_file: | |
| 521 # Compute a single diff for all files and parse the output; should | |
| 522 # with git this is much faster than computing one diff for each file. | |
| 523 diffs = {} | |
| 524 | |
| 525 # Don't specify any filenames below, because there are command line length | |
| 526 # limits on some platforms and GenerateDiff would fail. | |
| 527 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True) | |
| 528 | |
| 529 # This regex matches the path twice, separated by a space. Note that | |
| 530 # filename itself may contain spaces. | |
| 531 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$') | |
|
iannucci
2013/06/07 02:17:36
won't there be a/ and b/ here? Or does scm.GIT do
ncarter (slow)
2013/06/07 19:51:15
the a/ and b/ are omitted when the --no-prefix opt
| |
| 532 current_diff = [] | |
| 533 for x in unified_diff.splitlines(True): | |
|
iannucci
2013/06/07 02:17:36
Since keepends is infrequently used, let's invoke
ncarter (slow)
2013/06/07 19:51:15
I agree, but actually splitlines is one of those p
iannucci
2013/06/07 21:30:16
That's a bummer. New version lg though.
| |
| 534 match = file_marker.match(x) | |
| 535 if match: | |
| 536 # Marks the start of a new per-file section. | |
| 537 diffs[match.group('filename')] = current_diff = [x] | |
| 538 elif x.startswith('diff --git'): | |
| 539 raise PresubmitFailure('Unexpected diff line: %s' % x) | |
| 540 else: | |
| 541 current_diff.append(x) | |
| 542 | |
| 543 self._diffs_by_file = dict( | |
| 544 (normpath(path), ''.join(diff)) for path, diff in diffs.items()) | |
| 545 | |
| 546 if path not in self._diffs_by_file: | |
| 547 raise PresubmitFailure( | |
| 548 'Unified diff did not contain entry for file %s' % path) | |
| 549 | |
| 550 return self._diffs_by_file[path] | |
| 551 | |
| 552 | |
| 492 class AffectedFile(object): | 553 class AffectedFile(object): |
| 493 """Representation of a file in a change.""" | 554 """Representation of a file in a change.""" |
| 555 | |
| 556 DIFF_CACHE = _DiffCache | |
| 557 | |
| 494 # Method could be a function | 558 # Method could be a function |
| 495 # pylint: disable=R0201 | 559 # pylint: disable=R0201 |
| 496 def __init__(self, path, action, repository_root): | 560 def __init__(self, path, action, repository_root, diff_cache=None): |
| 497 self._path = path | 561 self._path = path |
| 498 self._action = action | 562 self._action = action |
| 499 self._local_root = repository_root | 563 self._local_root = repository_root |
| 500 self._is_directory = None | 564 self._is_directory = None |
| 501 self._properties = {} | 565 self._properties = {} |
| 502 self._cached_changed_contents = None | 566 self._cached_changed_contents = None |
| 503 self._cached_new_contents = None | 567 self._cached_new_contents = None |
| 568 if diff_cache: | |
| 569 self._diff_cache = diff_cache | |
| 570 else: | |
| 571 self._diff_cache = self.DIFF_CACHE() | |
| 504 logging.debug('%s(%s)' % (self.__class__.__name__, self._path)) | 572 logging.debug('%s(%s)' % (self.__class__.__name__, self._path)) |
| 505 | 573 |
| 506 def ServerPath(self): | 574 def ServerPath(self): |
| 507 """Returns a path string that identifies the file in the SCM system. | 575 """Returns a path string that identifies the file in the SCM system. |
| 508 | 576 |
| 509 Returns the empty string if the file does not exist in SCM. | 577 Returns the empty string if the file does not exist in SCM. |
| 510 """ | 578 """ |
| 511 return "" | 579 return '' |
| 512 | 580 |
| 513 def LocalPath(self): | 581 def LocalPath(self): |
| 514 """Returns the path of this file on the local disk relative to client root. | 582 """Returns the path of this file on the local disk relative to client root. |
| 515 """ | 583 """ |
| 516 return normpath(self._path) | 584 return normpath(self._path) |
| 517 | 585 |
| 518 def AbsoluteLocalPath(self): | 586 def AbsoluteLocalPath(self): |
| 519 """Returns the absolute path of this file on the local disk. | 587 """Returns the absolute path of this file on the local disk. |
| 520 """ | 588 """ |
| 521 return os.path.abspath(os.path.join(self._local_root, self.LocalPath())) | 589 return os.path.abspath(os.path.join(self._local_root, self.LocalPath())) |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 558 if self._cached_new_contents is None: | 626 if self._cached_new_contents is None: |
| 559 self._cached_new_contents = [] | 627 self._cached_new_contents = [] |
| 560 if not self.IsDirectory(): | 628 if not self.IsDirectory(): |
| 561 try: | 629 try: |
| 562 self._cached_new_contents = gclient_utils.FileRead( | 630 self._cached_new_contents = gclient_utils.FileRead( |
| 563 self.AbsoluteLocalPath(), 'rU').splitlines() | 631 self.AbsoluteLocalPath(), 'rU').splitlines() |
| 564 except IOError: | 632 except IOError: |
| 565 pass # File not found? That's fine; maybe it was deleted. | 633 pass # File not found? That's fine; maybe it was deleted. |
| 566 return self._cached_new_contents[:] | 634 return self._cached_new_contents[:] |
| 567 | 635 |
| 568 def OldContents(self): | |
| 569 """Returns an iterator over the lines in the old version of file. | |
| 570 | |
| 571 The old version is the file in depot, i.e. the "left hand side". | |
| 572 """ | |
| 573 raise NotImplementedError() # Implement when needed | |
| 574 | |
| 575 def OldFileTempPath(self): | |
| 576 """Returns the path on local disk where the old contents resides. | |
| 577 | |
| 578 The old version is the file in depot, i.e. the "left hand side". | |
| 579 This is a read-only cached copy of the old contents. *DO NOT* try to | |
| 580 modify this file. | |
| 581 """ | |
| 582 raise NotImplementedError() # Implement if/when needed. | |
| 583 | |
| 584 def ChangedContents(self): | 636 def ChangedContents(self): |
| 585 """Returns a list of tuples (line number, line text) of all new lines. | 637 """Returns a list of tuples (line number, line text) of all new lines. |
| 586 | 638 |
| 587 This relies on the scm diff output describing each changed code section | 639 This relies on the scm diff output describing each changed code section |
| 588 with a line of the form | 640 with a line of the form |
| 589 | 641 |
| 590 ^@@ <old line num>,<old size> <new line num>,<new size> @@$ | 642 ^@@ <old line num>,<old size> <new line num>,<new size> @@$ |
| 591 """ | 643 """ |
| 592 if self._cached_changed_contents is not None: | 644 if self._cached_changed_contents is not None: |
| 593 return self._cached_changed_contents[:] | 645 return self._cached_changed_contents[:] |
| (...skipping 11 matching lines...) Expand all Loading... | |
| 605 if line.startswith('+') and not line.startswith('++'): | 657 if line.startswith('+') and not line.startswith('++'): |
| 606 self._cached_changed_contents.append((line_num, line[1:])) | 658 self._cached_changed_contents.append((line_num, line[1:])) |
| 607 if not line.startswith('-'): | 659 if not line.startswith('-'): |
| 608 line_num += 1 | 660 line_num += 1 |
| 609 return self._cached_changed_contents[:] | 661 return self._cached_changed_contents[:] |
| 610 | 662 |
| 611 def __str__(self): | 663 def __str__(self): |
| 612 return self.LocalPath() | 664 return self.LocalPath() |
| 613 | 665 |
| 614 def GenerateScmDiff(self): | 666 def GenerateScmDiff(self): |
| 615 raise NotImplementedError() # Implemented in derived classes. | 667 return self._diff_cache.GetScmDiff(self.LocalPath(), self._local_root) |
| 616 | 668 |
| 617 | 669 |
| 618 class SvnAffectedFile(AffectedFile): | 670 class SvnAffectedFile(AffectedFile): |
| 619 """Representation of a file in a change out of a Subversion checkout.""" | 671 """Representation of a file in a change out of a Subversion checkout.""" |
| 620 # Method 'NNN' is abstract in class 'NNN' but is not overridden | 672 # Method 'NNN' is abstract in class 'NNN' but is not overridden |
| 621 # pylint: disable=W0223 | 673 # pylint: disable=W0223 |
| 622 | 674 |
| 675 DIFF_CACHE = _SvnDiffCache | |
| 676 | |
| 623 def __init__(self, *args, **kwargs): | 677 def __init__(self, *args, **kwargs): |
| 624 AffectedFile.__init__(self, *args, **kwargs) | 678 AffectedFile.__init__(self, *args, **kwargs) |
| 625 self._server_path = None | 679 self._server_path = None |
| 626 self._is_text_file = None | 680 self._is_text_file = None |
| 627 self._diff = None | |
| 628 | 681 |
| 629 def ServerPath(self): | 682 def ServerPath(self): |
| 630 if self._server_path is None: | 683 if self._server_path is None: |
| 631 self._server_path = scm.SVN.CaptureLocalInfo( | 684 self._server_path = scm.SVN.CaptureLocalInfo( |
| 632 [self.LocalPath()], self._local_root).get('URL', '') | 685 [self.LocalPath()], self._local_root).get('URL', '') |
| 633 return self._server_path | 686 return self._server_path |
| 634 | 687 |
| 635 def IsDirectory(self): | 688 def IsDirectory(self): |
| 636 if self._is_directory is None: | 689 if self._is_directory is None: |
| 637 path = self.AbsoluteLocalPath() | 690 path = self.AbsoluteLocalPath() |
| (...skipping 19 matching lines...) Expand all Loading... | |
| 657 # A deleted file is not a text file. | 710 # A deleted file is not a text file. |
| 658 self._is_text_file = False | 711 self._is_text_file = False |
| 659 elif self.IsDirectory(): | 712 elif self.IsDirectory(): |
| 660 self._is_text_file = False | 713 self._is_text_file = False |
| 661 else: | 714 else: |
| 662 mime_type = scm.SVN.GetFileProperty( | 715 mime_type = scm.SVN.GetFileProperty( |
| 663 self.LocalPath(), 'svn:mime-type', self._local_root) | 716 self.LocalPath(), 'svn:mime-type', self._local_root) |
| 664 self._is_text_file = (not mime_type or mime_type.startswith('text/')) | 717 self._is_text_file = (not mime_type or mime_type.startswith('text/')) |
| 665 return self._is_text_file | 718 return self._is_text_file |
| 666 | 719 |
| 667 def GenerateScmDiff(self): | |
| 668 if self._diff is None: | |
| 669 self._diff = scm.SVN.GenerateDiff( | |
| 670 [self.LocalPath()], self._local_root, False, None) | |
| 671 return self._diff | |
| 672 | |
| 673 | 720 |
| 674 class GitAffectedFile(AffectedFile): | 721 class GitAffectedFile(AffectedFile): |
| 675 """Representation of a file in a change out of a git checkout.""" | 722 """Representation of a file in a change out of a git checkout.""" |
| 676 # Method 'NNN' is abstract in class 'NNN' but is not overridden | 723 # Method 'NNN' is abstract in class 'NNN' but is not overridden |
| 677 # pylint: disable=W0223 | 724 # pylint: disable=W0223 |
| 678 | 725 |
| 726 DIFF_CACHE = _GitDiffCache | |
| 727 | |
| 679 def __init__(self, *args, **kwargs): | 728 def __init__(self, *args, **kwargs): |
| 680 AffectedFile.__init__(self, *args, **kwargs) | 729 AffectedFile.__init__(self, *args, **kwargs) |
| 681 self._server_path = None | 730 self._server_path = None |
| 682 self._is_text_file = None | 731 self._is_text_file = None |
| 683 self._diff = None | |
| 684 | 732 |
| 685 def ServerPath(self): | 733 def ServerPath(self): |
| 686 if self._server_path is None: | 734 if self._server_path is None: |
| 687 raise NotImplementedError('TODO(maruel) Implement.') | 735 raise NotImplementedError('TODO(maruel) Implement.') |
| 688 return self._server_path | 736 return self._server_path |
| 689 | 737 |
| 690 def IsDirectory(self): | 738 def IsDirectory(self): |
| 691 if self._is_directory is None: | 739 if self._is_directory is None: |
| 692 path = self.AbsoluteLocalPath() | 740 path = self.AbsoluteLocalPath() |
| 693 if os.path.exists(path): | 741 if os.path.exists(path): |
| (...skipping 13 matching lines...) Expand all Loading... | |
| 707 if self._is_text_file is None: | 755 if self._is_text_file is None: |
| 708 if self.Action() == 'D': | 756 if self.Action() == 'D': |
| 709 # A deleted file is not a text file. | 757 # A deleted file is not a text file. |
| 710 self._is_text_file = False | 758 self._is_text_file = False |
| 711 elif self.IsDirectory(): | 759 elif self.IsDirectory(): |
| 712 self._is_text_file = False | 760 self._is_text_file = False |
| 713 else: | 761 else: |
| 714 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath()) | 762 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath()) |
| 715 return self._is_text_file | 763 return self._is_text_file |
| 716 | 764 |
| 717 def GenerateScmDiff(self): | |
| 718 if self._diff is None: | |
| 719 self._diff = scm.GIT.GenerateDiff( | |
| 720 self._local_root, files=[self.LocalPath(),]) | |
| 721 return self._diff | |
| 722 | |
| 723 | 765 |
| 724 class Change(object): | 766 class Change(object): |
| 725 """Describe a change. | 767 """Describe a change. |
| 726 | 768 |
| 727 Used directly by the presubmit scripts to query the current change being | 769 Used directly by the presubmit scripts to query the current change being |
| 728 tested. | 770 tested. |
| 729 | 771 |
| 730 Instance members: | 772 Instance members: |
| 731 tags: Dictionnary of KEY=VALUE pairs found in the change description. | 773 tags: Dictionary of KEY=VALUE pairs found in the change description. |
| 732 self.KEY: equivalent to tags['KEY'] | 774 self.KEY: equivalent to tags['KEY'] |
| 733 """ | 775 """ |
| 734 | 776 |
| 735 _AFFECTED_FILES = AffectedFile | 777 _AFFECTED_FILES = AffectedFile |
| 736 | 778 |
| 737 # Matches key/value (or "tag") lines in changelist descriptions. | 779 # Matches key/value (or "tag") lines in changelist descriptions. |
| 738 TAG_LINE_RE = re.compile( | 780 TAG_LINE_RE = re.compile( |
| 739 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$') | 781 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$') |
| 740 scm = '' | 782 scm = '' |
| 741 | 783 |
| (...skipping 20 matching lines...) Expand all Loading... | |
| 762 else: | 804 else: |
| 763 description_without_tags.append(line) | 805 description_without_tags.append(line) |
| 764 | 806 |
| 765 # Change back to text and remove whitespace at end. | 807 # Change back to text and remove whitespace at end. |
| 766 self._description_without_tags = ( | 808 self._description_without_tags = ( |
| 767 '\n'.join(description_without_tags).rstrip()) | 809 '\n'.join(description_without_tags).rstrip()) |
| 768 | 810 |
| 769 assert all( | 811 assert all( |
| 770 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files | 812 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files |
| 771 | 813 |
| 814 diff_cache = self._AFFECTED_FILES.DIFF_CACHE() | |
| 772 self._affected_files = [ | 815 self._affected_files = [ |
| 773 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root) | 816 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache) |
| 774 for info in files | 817 for action, path in files |
| 775 ] | 818 ] |
| 776 | 819 |
| 777 def Name(self): | 820 def Name(self): |
| 778 """Returns the change name.""" | 821 """Returns the change name.""" |
| 779 return self._name | 822 return self._name |
| 780 | 823 |
| 781 def DescriptionText(self): | 824 def DescriptionText(self): |
| 782 """Returns the user-entered changelist description, minus tags. | 825 """Returns the user-entered changelist description, minus tags. |
| 783 | 826 |
| 784 Any line in the user-provided description starting with e.g. "FOO=" | 827 Any line in the user-provided description starting with e.g. "FOO=" |
| (...skipping 598 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1383 except PresubmitFailure, e: | 1426 except PresubmitFailure, e: |
| 1384 print >> sys.stderr, e | 1427 print >> sys.stderr, e |
| 1385 print >> sys.stderr, 'Maybe your depot_tools is out of date?' | 1428 print >> sys.stderr, 'Maybe your depot_tools is out of date?' |
| 1386 print >> sys.stderr, 'If all fails, contact maruel@' | 1429 print >> sys.stderr, 'If all fails, contact maruel@' |
| 1387 return 2 | 1430 return 2 |
| 1388 | 1431 |
| 1389 | 1432 |
| 1390 if __name__ == '__main__': | 1433 if __name__ == '__main__': |
| 1391 fix_encoding.fix_encoding() | 1434 fix_encoding.fix_encoding() |
| 1392 sys.exit(Main(None)) | 1435 sys.exit(Main(None)) |
| OLD | NEW |