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): |
| 493 """Caches diffs retrieved from a particular SCM.""" |
| 494 |
| 495 def GetDiff(self, path, local_root): |
| 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 super(_SvnDiffCache, self).__init__() |
| 504 self._diffs_by_file = {} |
| 505 |
| 506 def GetDiff(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 super(_GitDiffCache, self).__init__() |
| 517 self._diffs_by_file = None |
| 518 |
| 519 def GetDiff(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)$') |
| 532 current_diff = [] |
| 533 keep_line_endings = True |
| 534 for x in unified_diff.splitlines(keep_line_endings): |
| 535 match = file_marker.match(x) |
| 536 if match: |
| 537 # Marks the start of a new per-file section. |
| 538 diffs[match.group('filename')] = current_diff = [x] |
| 539 elif x.startswith('diff --git'): |
| 540 raise PresubmitFailure('Unexpected diff line: %s' % x) |
| 541 else: |
| 542 current_diff.append(x) |
| 543 |
| 544 self._diffs_by_file = dict( |
| 545 (normpath(path), ''.join(diff)) for path, diff in diffs.items()) |
| 546 |
| 547 if path not in self._diffs_by_file: |
| 548 raise PresubmitFailure( |
| 549 'Unified diff did not contain entry for file %s' % path) |
| 550 |
| 551 return self._diffs_by_file[path] |
| 552 |
| 553 |
492 class AffectedFile(object): | 554 class AffectedFile(object): |
493 """Representation of a file in a change.""" | 555 """Representation of a file in a change.""" |
| 556 |
| 557 DIFF_CACHE = _DiffCache |
| 558 |
494 # Method could be a function | 559 # Method could be a function |
495 # pylint: disable=R0201 | 560 # pylint: disable=R0201 |
496 def __init__(self, path, action, repository_root): | 561 def __init__(self, path, action, repository_root, diff_cache=None): |
497 self._path = path | 562 self._path = path |
498 self._action = action | 563 self._action = action |
499 self._local_root = repository_root | 564 self._local_root = repository_root |
500 self._is_directory = None | 565 self._is_directory = None |
501 self._properties = {} | 566 self._properties = {} |
502 self._cached_changed_contents = None | 567 self._cached_changed_contents = None |
503 self._cached_new_contents = None | 568 self._cached_new_contents = None |
| 569 if diff_cache: |
| 570 self._diff_cache = diff_cache |
| 571 else: |
| 572 self._diff_cache = self.DIFF_CACHE() |
504 logging.debug('%s(%s)' % (self.__class__.__name__, self._path)) | 573 logging.debug('%s(%s)' % (self.__class__.__name__, self._path)) |
505 | 574 |
506 def ServerPath(self): | 575 def ServerPath(self): |
507 """Returns a path string that identifies the file in the SCM system. | 576 """Returns a path string that identifies the file in the SCM system. |
508 | 577 |
509 Returns the empty string if the file does not exist in SCM. | 578 Returns the empty string if the file does not exist in SCM. |
510 """ | 579 """ |
511 return "" | 580 return '' |
512 | 581 |
513 def LocalPath(self): | 582 def LocalPath(self): |
514 """Returns the path of this file on the local disk relative to client root. | 583 """Returns the path of this file on the local disk relative to client root. |
515 """ | 584 """ |
516 return normpath(self._path) | 585 return normpath(self._path) |
517 | 586 |
518 def AbsoluteLocalPath(self): | 587 def AbsoluteLocalPath(self): |
519 """Returns the absolute path of this file on the local disk. | 588 """Returns the absolute path of this file on the local disk. |
520 """ | 589 """ |
521 return os.path.abspath(os.path.join(self._local_root, self.LocalPath())) | 590 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: | 627 if self._cached_new_contents is None: |
559 self._cached_new_contents = [] | 628 self._cached_new_contents = [] |
560 if not self.IsDirectory(): | 629 if not self.IsDirectory(): |
561 try: | 630 try: |
562 self._cached_new_contents = gclient_utils.FileRead( | 631 self._cached_new_contents = gclient_utils.FileRead( |
563 self.AbsoluteLocalPath(), 'rU').splitlines() | 632 self.AbsoluteLocalPath(), 'rU').splitlines() |
564 except IOError: | 633 except IOError: |
565 pass # File not found? That's fine; maybe it was deleted. | 634 pass # File not found? That's fine; maybe it was deleted. |
566 return self._cached_new_contents[:] | 635 return self._cached_new_contents[:] |
567 | 636 |
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): | 637 def ChangedContents(self): |
585 """Returns a list of tuples (line number, line text) of all new lines. | 638 """Returns a list of tuples (line number, line text) of all new lines. |
586 | 639 |
587 This relies on the scm diff output describing each changed code section | 640 This relies on the scm diff output describing each changed code section |
588 with a line of the form | 641 with a line of the form |
589 | 642 |
590 ^@@ <old line num>,<old size> <new line num>,<new size> @@$ | 643 ^@@ <old line num>,<old size> <new line num>,<new size> @@$ |
591 """ | 644 """ |
592 if self._cached_changed_contents is not None: | 645 if self._cached_changed_contents is not None: |
593 return self._cached_changed_contents[:] | 646 return self._cached_changed_contents[:] |
(...skipping 11 matching lines...) Expand all Loading... |
605 if line.startswith('+') and not line.startswith('++'): | 658 if line.startswith('+') and not line.startswith('++'): |
606 self._cached_changed_contents.append((line_num, line[1:])) | 659 self._cached_changed_contents.append((line_num, line[1:])) |
607 if not line.startswith('-'): | 660 if not line.startswith('-'): |
608 line_num += 1 | 661 line_num += 1 |
609 return self._cached_changed_contents[:] | 662 return self._cached_changed_contents[:] |
610 | 663 |
611 def __str__(self): | 664 def __str__(self): |
612 return self.LocalPath() | 665 return self.LocalPath() |
613 | 666 |
614 def GenerateScmDiff(self): | 667 def GenerateScmDiff(self): |
615 raise NotImplementedError() # Implemented in derived classes. | 668 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root) |
616 | 669 |
617 | 670 |
618 class SvnAffectedFile(AffectedFile): | 671 class SvnAffectedFile(AffectedFile): |
619 """Representation of a file in a change out of a Subversion checkout.""" | 672 """Representation of a file in a change out of a Subversion checkout.""" |
620 # Method 'NNN' is abstract in class 'NNN' but is not overridden | 673 # Method 'NNN' is abstract in class 'NNN' but is not overridden |
621 # pylint: disable=W0223 | 674 # pylint: disable=W0223 |
622 | 675 |
| 676 DIFF_CACHE = _SvnDiffCache |
| 677 |
623 def __init__(self, *args, **kwargs): | 678 def __init__(self, *args, **kwargs): |
624 AffectedFile.__init__(self, *args, **kwargs) | 679 AffectedFile.__init__(self, *args, **kwargs) |
625 self._server_path = None | 680 self._server_path = None |
626 self._is_text_file = None | 681 self._is_text_file = None |
627 self._diff = None | |
628 | 682 |
629 def ServerPath(self): | 683 def ServerPath(self): |
630 if self._server_path is None: | 684 if self._server_path is None: |
631 self._server_path = scm.SVN.CaptureLocalInfo( | 685 self._server_path = scm.SVN.CaptureLocalInfo( |
632 [self.LocalPath()], self._local_root).get('URL', '') | 686 [self.LocalPath()], self._local_root).get('URL', '') |
633 return self._server_path | 687 return self._server_path |
634 | 688 |
635 def IsDirectory(self): | 689 def IsDirectory(self): |
636 if self._is_directory is None: | 690 if self._is_directory is None: |
637 path = self.AbsoluteLocalPath() | 691 path = self.AbsoluteLocalPath() |
(...skipping 19 matching lines...) Expand all Loading... |
657 # A deleted file is not a text file. | 711 # A deleted file is not a text file. |
658 self._is_text_file = False | 712 self._is_text_file = False |
659 elif self.IsDirectory(): | 713 elif self.IsDirectory(): |
660 self._is_text_file = False | 714 self._is_text_file = False |
661 else: | 715 else: |
662 mime_type = scm.SVN.GetFileProperty( | 716 mime_type = scm.SVN.GetFileProperty( |
663 self.LocalPath(), 'svn:mime-type', self._local_root) | 717 self.LocalPath(), 'svn:mime-type', self._local_root) |
664 self._is_text_file = (not mime_type or mime_type.startswith('text/')) | 718 self._is_text_file = (not mime_type or mime_type.startswith('text/')) |
665 return self._is_text_file | 719 return self._is_text_file |
666 | 720 |
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 | 721 |
674 class GitAffectedFile(AffectedFile): | 722 class GitAffectedFile(AffectedFile): |
675 """Representation of a file in a change out of a git checkout.""" | 723 """Representation of a file in a change out of a git checkout.""" |
676 # Method 'NNN' is abstract in class 'NNN' but is not overridden | 724 # Method 'NNN' is abstract in class 'NNN' but is not overridden |
677 # pylint: disable=W0223 | 725 # pylint: disable=W0223 |
678 | 726 |
| 727 DIFF_CACHE = _GitDiffCache |
| 728 |
679 def __init__(self, *args, **kwargs): | 729 def __init__(self, *args, **kwargs): |
680 AffectedFile.__init__(self, *args, **kwargs) | 730 AffectedFile.__init__(self, *args, **kwargs) |
681 self._server_path = None | 731 self._server_path = None |
682 self._is_text_file = None | 732 self._is_text_file = None |
683 self._diff = None | |
684 | 733 |
685 def ServerPath(self): | 734 def ServerPath(self): |
686 if self._server_path is None: | 735 if self._server_path is None: |
687 raise NotImplementedError('TODO(maruel) Implement.') | 736 raise NotImplementedError('TODO(maruel) Implement.') |
688 return self._server_path | 737 return self._server_path |
689 | 738 |
690 def IsDirectory(self): | 739 def IsDirectory(self): |
691 if self._is_directory is None: | 740 if self._is_directory is None: |
692 path = self.AbsoluteLocalPath() | 741 path = self.AbsoluteLocalPath() |
693 if os.path.exists(path): | 742 if os.path.exists(path): |
(...skipping 13 matching lines...) Expand all Loading... |
707 if self._is_text_file is None: | 756 if self._is_text_file is None: |
708 if self.Action() == 'D': | 757 if self.Action() == 'D': |
709 # A deleted file is not a text file. | 758 # A deleted file is not a text file. |
710 self._is_text_file = False | 759 self._is_text_file = False |
711 elif self.IsDirectory(): | 760 elif self.IsDirectory(): |
712 self._is_text_file = False | 761 self._is_text_file = False |
713 else: | 762 else: |
714 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath()) | 763 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath()) |
715 return self._is_text_file | 764 return self._is_text_file |
716 | 765 |
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 | 766 |
724 class Change(object): | 767 class Change(object): |
725 """Describe a change. | 768 """Describe a change. |
726 | 769 |
727 Used directly by the presubmit scripts to query the current change being | 770 Used directly by the presubmit scripts to query the current change being |
728 tested. | 771 tested. |
729 | 772 |
730 Instance members: | 773 Instance members: |
731 tags: Dictionnary of KEY=VALUE pairs found in the change description. | 774 tags: Dictionary of KEY=VALUE pairs found in the change description. |
732 self.KEY: equivalent to tags['KEY'] | 775 self.KEY: equivalent to tags['KEY'] |
733 """ | 776 """ |
734 | 777 |
735 _AFFECTED_FILES = AffectedFile | 778 _AFFECTED_FILES = AffectedFile |
736 | 779 |
737 # Matches key/value (or "tag") lines in changelist descriptions. | 780 # Matches key/value (or "tag") lines in changelist descriptions. |
738 TAG_LINE_RE = re.compile( | 781 TAG_LINE_RE = re.compile( |
739 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$') | 782 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$') |
740 scm = '' | 783 scm = '' |
741 | 784 |
(...skipping 20 matching lines...) Expand all Loading... |
762 else: | 805 else: |
763 description_without_tags.append(line) | 806 description_without_tags.append(line) |
764 | 807 |
765 # Change back to text and remove whitespace at end. | 808 # Change back to text and remove whitespace at end. |
766 self._description_without_tags = ( | 809 self._description_without_tags = ( |
767 '\n'.join(description_without_tags).rstrip()) | 810 '\n'.join(description_without_tags).rstrip()) |
768 | 811 |
769 assert all( | 812 assert all( |
770 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files | 813 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files |
771 | 814 |
| 815 diff_cache = self._AFFECTED_FILES.DIFF_CACHE() |
772 self._affected_files = [ | 816 self._affected_files = [ |
773 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root) | 817 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache) |
774 for info in files | 818 for action, path in files |
775 ] | 819 ] |
776 | 820 |
777 def Name(self): | 821 def Name(self): |
778 """Returns the change name.""" | 822 """Returns the change name.""" |
779 return self._name | 823 return self._name |
780 | 824 |
781 def DescriptionText(self): | 825 def DescriptionText(self): |
782 """Returns the user-entered changelist description, minus tags. | 826 """Returns the user-entered changelist description, minus tags. |
783 | 827 |
784 Any line in the user-provided description starting with e.g. "FOO=" | 828 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: | 1427 except PresubmitFailure, e: |
1384 print >> sys.stderr, e | 1428 print >> sys.stderr, e |
1385 print >> sys.stderr, 'Maybe your depot_tools is out of date?' | 1429 print >> sys.stderr, 'Maybe your depot_tools is out of date?' |
1386 print >> sys.stderr, 'If all fails, contact maruel@' | 1430 print >> sys.stderr, 'If all fails, contact maruel@' |
1387 return 2 | 1431 return 2 |
1388 | 1432 |
1389 | 1433 |
1390 if __name__ == '__main__': | 1434 if __name__ == '__main__': |
1391 fix_encoding.fix_encoding() | 1435 fix_encoding.fix_encoding() |
1392 sys.exit(Main(None)) | 1436 sys.exit(Main(None)) |
OLD | NEW |