Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(235)

Side by Side Diff: presubmit_support.py

Issue 15898005: presubmit: Call 'git diff' once per change instead of once per file (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: Fix language in git_cl.py Created 7 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « presubmit_canned_checks.py ('k') | tests/presubmit_unittest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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
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
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
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))
OLDNEW
« no previous file with comments | « presubmit_canned_checks.py ('k') | tests/presubmit_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698