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

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 unit test on Windows. 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
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):
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
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
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
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
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
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
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))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698