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.8.0' | 9 __version__ = '1.8.0' |
10 | 10 |
(...skipping 393 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
404 def PresubmitLocalPath(self): | 404 def PresubmitLocalPath(self): |
405 """Returns the local path of the presubmit script currently being run. | 405 """Returns the local path of the presubmit script currently being run. |
406 | 406 |
407 This is useful if you don't want to hard-code absolute paths in the | 407 This is useful if you don't want to hard-code absolute paths in the |
408 presubmit script. For example, It can be used to find another file | 408 presubmit script. For example, It can be used to find another file |
409 relative to the PRESUBMIT.py script, so the whole tree can be branched and | 409 relative to the PRESUBMIT.py script, so the whole tree can be branched and |
410 the presubmit script still works, without editing its content. | 410 the presubmit script still works, without editing its content. |
411 """ | 411 """ |
412 return self._current_presubmit_path | 412 return self._current_presubmit_path |
413 | 413 |
414 def DepotToLocalPath(self, depot_path): | 414 def AffectedFiles(self, include_deletes=True, file_filter=None): |
415 """Translate a depot path to a local path (relative to client root). | |
416 | |
417 Args: | |
418 Depot path as a string. | |
419 | |
420 Returns: | |
421 The local path of the depot path under the user's current client, or None | |
422 if the file is not mapped. | |
423 | |
424 Remember to check for the None case and show an appropriate error! | |
425 """ | |
426 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot() | |
427 ).get('Path') | |
428 | |
429 def LocalToDepotPath(self, local_path): | |
430 """Translate a local path to a depot path. | |
431 | |
432 Args: | |
433 Local path (relative to current directory, or absolute) as a string. | |
434 | |
435 Returns: | |
436 The depot path (SVN URL) of the file if mapped, otherwise None. | |
437 """ | |
438 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot() | |
439 ).get('URL') | |
440 | |
441 def AffectedFiles(self, include_dirs=False, include_deletes=True, | |
442 file_filter=None): | |
443 """Same as input_api.change.AffectedFiles() except only lists files | 415 """Same as input_api.change.AffectedFiles() except only lists files |
444 (and optionally directories) in the same directory as the current presubmit | 416 (and optionally directories) in the same directory as the current presubmit |
445 script, or subdirectories thereof. | 417 script, or subdirectories thereof. |
446 """ | 418 """ |
447 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath()) | 419 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath()) |
448 if len(dir_with_slash) == 1: | 420 if len(dir_with_slash) == 1: |
449 dir_with_slash = '' | 421 dir_with_slash = '' |
450 | 422 |
451 return filter( | 423 return filter( |
452 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash), | 424 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash), |
453 self.change.AffectedFiles(include_dirs, include_deletes, file_filter)) | 425 self.change.AffectedFiles(include_deletes, file_filter)) |
454 | 426 |
455 def LocalPaths(self, include_dirs=False): | 427 def LocalPaths(self): |
456 """Returns local paths of input_api.AffectedFiles().""" | 428 """Returns local paths of input_api.AffectedFiles().""" |
457 paths = [af.LocalPath() for af in self.AffectedFiles(include_dirs)] | 429 paths = [af.LocalPath() for af in self.AffectedFiles()] |
458 logging.debug("LocalPaths: %s", paths) | 430 logging.debug("LocalPaths: %s", paths) |
459 return paths | 431 return paths |
460 | 432 |
461 def AbsoluteLocalPaths(self, include_dirs=False): | 433 def AbsoluteLocalPaths(self): |
462 """Returns absolute local paths of input_api.AffectedFiles().""" | 434 """Returns absolute local paths of input_api.AffectedFiles().""" |
463 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)] | 435 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()] |
464 | 436 |
465 def ServerPaths(self, include_dirs=False): | 437 def AffectedTestableFiles(self, include_deletes=None): |
M-A Ruel
2016/10/05 21:25:33
This increases the odd of this CL blowing up. Are
agable
2016/10/05 22:51:00
Sigh, you're right, it would kill a bunch of PRESU
| |
466 """Returns server paths of input_api.AffectedFiles().""" | 438 """Same as input_api.change.AffectedTestableFiles() except only lists files |
467 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)] | |
468 | |
469 def AffectedTextFiles(self, include_deletes=None): | |
470 """Same as input_api.change.AffectedTextFiles() except only lists files | |
471 in the same directory as the current presubmit script, or subdirectories | 439 in the same directory as the current presubmit script, or subdirectories |
472 thereof. | 440 thereof. |
473 """ | 441 """ |
474 if include_deletes is not None: | 442 if include_deletes is not None: |
475 warn("AffectedTextFiles(include_deletes=%s)" | 443 warn("AffectedTestableFiles(include_deletes=%s)" |
476 " is deprecated and ignored" % str(include_deletes), | 444 " is deprecated and ignored" % str(include_deletes), |
477 category=DeprecationWarning, | 445 category=DeprecationWarning, |
478 stacklevel=2) | 446 stacklevel=2) |
479 return filter(lambda x: x.IsTextFile(), | 447 return filter(lambda x: x.IsTestableFile(), |
480 self.AffectedFiles(include_dirs=False, include_deletes=False)) | 448 self.AffectedFiles(include_deletes=False)) |
481 | 449 |
482 def FilterSourceFile(self, affected_file, white_list=None, black_list=None): | 450 def FilterSourceFile(self, affected_file, white_list=None, black_list=None): |
483 """Filters out files that aren't considered "source file". | 451 """Filters out files that aren't considered "source file". |
484 | 452 |
485 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST | 453 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST |
486 and InputApi.DEFAULT_BLACK_LIST is used respectively. | 454 and InputApi.DEFAULT_BLACK_LIST is used respectively. |
487 | 455 |
488 The lists will be compiled as regular expression and | 456 The lists will be compiled as regular expression and |
489 AffectedFile.LocalPath() needs to pass both list. | 457 AffectedFile.LocalPath() needs to pass both list. |
490 | 458 |
491 Note: Copy-paste this function to suit your needs or use a lambda function. | 459 Note: Copy-paste this function to suit your needs or use a lambda function. |
492 """ | 460 """ |
493 def Find(affected_file, items): | 461 def Find(affected_file, items): |
494 local_path = affected_file.LocalPath() | 462 local_path = affected_file.LocalPath() |
495 for item in items: | 463 for item in items: |
496 if self.re.match(item, local_path): | 464 if self.re.match(item, local_path): |
497 return True | 465 return True |
498 return False | 466 return False |
499 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and | 467 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and |
500 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST)) | 468 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST)) |
501 | 469 |
502 def AffectedSourceFiles(self, source_file): | 470 def AffectedSourceFiles(self, source_file): |
503 """Filter the list of AffectedTextFiles by the function source_file. | 471 """Filter the list of AffectedTestableFiles by the function source_file. |
504 | 472 |
505 If source_file is None, InputApi.FilterSourceFile() is used. | 473 If source_file is None, InputApi.FilterSourceFile() is used. |
506 """ | 474 """ |
507 if not source_file: | 475 if not source_file: |
508 source_file = self.FilterSourceFile | 476 source_file = self.FilterSourceFile |
509 return filter(source_file, self.AffectedTextFiles()) | 477 return filter(source_file, self.AffectedTestableFiles()) |
510 | 478 |
511 def RightHandSideLines(self, source_file_filter=None): | 479 def RightHandSideLines(self, source_file_filter=None): |
512 """An iterator over all text lines in "new" version of changed files. | 480 """An iterator over all text lines in "new" version of changed files. |
513 | 481 |
514 Only lists lines from new or modified text files in the change that are | 482 Only lists lines from new or modified text files in the change that are |
515 contained by the directory of the currently executing presubmit script. | 483 contained by the directory of the currently executing presubmit script. |
516 | 484 |
517 This is useful for doing line-by-line regex checks, like checking for | 485 This is useful for doing line-by-line regex checks, like checking for |
518 trailing whitespace. | 486 trailing whitespace. |
519 | 487 |
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
572 """Caches diffs retrieved from a particular SCM.""" | 540 """Caches diffs retrieved from a particular SCM.""" |
573 def __init__(self, upstream=None): | 541 def __init__(self, upstream=None): |
574 """Stores the upstream revision against which all diffs will be computed.""" | 542 """Stores the upstream revision against which all diffs will be computed.""" |
575 self._upstream = upstream | 543 self._upstream = upstream |
576 | 544 |
577 def GetDiff(self, path, local_root): | 545 def GetDiff(self, path, local_root): |
578 """Get the diff for a particular path.""" | 546 """Get the diff for a particular path.""" |
579 raise NotImplementedError() | 547 raise NotImplementedError() |
580 | 548 |
581 | 549 |
582 class _SvnDiffCache(_DiffCache): | |
583 """DiffCache implementation for subversion.""" | |
584 def __init__(self, *args, **kwargs): | |
585 super(_SvnDiffCache, self).__init__(*args, **kwargs) | |
586 self._diffs_by_file = {} | |
587 | |
588 def GetDiff(self, path, local_root): | |
589 if path not in self._diffs_by_file: | |
590 self._diffs_by_file[path] = scm.SVN.GenerateDiff([path], local_root, | |
591 False, None) | |
592 return self._diffs_by_file[path] | |
593 | |
594 | |
595 class _GitDiffCache(_DiffCache): | 550 class _GitDiffCache(_DiffCache): |
596 """DiffCache implementation for git; gets all file diffs at once.""" | 551 """DiffCache implementation for git; gets all file diffs at once.""" |
597 def __init__(self, upstream): | 552 def __init__(self, upstream): |
598 super(_GitDiffCache, self).__init__(upstream=upstream) | 553 super(_GitDiffCache, self).__init__(upstream=upstream) |
599 self._diffs_by_file = None | 554 self._diffs_by_file = None |
600 | 555 |
601 def GetDiff(self, path, local_root): | 556 def GetDiff(self, path, local_root): |
602 if not self._diffs_by_file: | 557 if not self._diffs_by_file: |
603 # Compute a single diff for all files and parse the output; should | 558 # Compute a single diff for all files and parse the output; should |
604 # with git this is much faster than computing one diff for each file. | 559 # with git this is much faster than computing one diff for each file. |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
639 | 594 |
640 DIFF_CACHE = _DiffCache | 595 DIFF_CACHE = _DiffCache |
641 | 596 |
642 # Method could be a function | 597 # Method could be a function |
643 # pylint: disable=R0201 | 598 # pylint: disable=R0201 |
644 def __init__(self, path, action, repository_root, diff_cache): | 599 def __init__(self, path, action, repository_root, diff_cache): |
645 self._path = path | 600 self._path = path |
646 self._action = action | 601 self._action = action |
647 self._local_root = repository_root | 602 self._local_root = repository_root |
648 self._is_directory = None | 603 self._is_directory = None |
649 self._properties = {} | |
650 self._cached_changed_contents = None | 604 self._cached_changed_contents = None |
651 self._cached_new_contents = None | 605 self._cached_new_contents = None |
652 self._diff_cache = diff_cache | 606 self._diff_cache = diff_cache |
653 logging.debug('%s(%s)', self.__class__.__name__, self._path) | 607 logging.debug('%s(%s)', self.__class__.__name__, self._path) |
654 | 608 |
655 def ServerPath(self): | |
656 """Returns a path string that identifies the file in the SCM system. | |
657 | |
658 Returns the empty string if the file does not exist in SCM. | |
659 """ | |
660 return '' | |
661 | |
662 def LocalPath(self): | 609 def LocalPath(self): |
663 """Returns the path of this file on the local disk relative to client root. | 610 """Returns the path of this file on the local disk relative to client root. |
664 """ | 611 """ |
665 return normpath(self._path) | 612 return normpath(self._path) |
666 | 613 |
667 def AbsoluteLocalPath(self): | 614 def AbsoluteLocalPath(self): |
668 """Returns the absolute path of this file on the local disk. | 615 """Returns the absolute path of this file on the local disk. |
669 """ | 616 """ |
670 return os.path.abspath(os.path.join(self._local_root, self.LocalPath())) | 617 return os.path.abspath(os.path.join(self._local_root, self.LocalPath())) |
671 | 618 |
672 def IsDirectory(self): | |
673 """Returns true if this object is a directory.""" | |
674 if self._is_directory is None: | |
675 path = self.AbsoluteLocalPath() | |
676 self._is_directory = (os.path.exists(path) and | |
677 os.path.isdir(path)) | |
678 return self._is_directory | |
679 | |
680 def Action(self): | 619 def Action(self): |
681 """Returns the action on this opened file, e.g. A, M, D, etc.""" | 620 """Returns the action on this opened file, e.g. A, M, D, etc.""" |
682 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but | 621 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but |
683 # different for other SCM. | 622 # different for other SCM. |
684 return self._action | 623 return self._action |
685 | 624 |
686 def Property(self, property_name): | 625 def IsTestableFile(self): |
687 """Returns the specified SCM property of this file, or None if no such | |
688 property. | |
689 """ | |
690 return self._properties.get(property_name, None) | |
691 | |
692 def IsTextFile(self): | |
693 """Returns True if the file is a text file and not a binary file. | 626 """Returns True if the file is a text file and not a binary file. |
694 | 627 |
695 Deleted files are not text file.""" | 628 Deleted files are not text file.""" |
696 raise NotImplementedError() # Implement when needed | 629 raise NotImplementedError() # Implement when needed |
697 | 630 |
698 def NewContents(self): | 631 def NewContents(self): |
699 """Returns an iterator over the lines in the new version of file. | 632 """Returns an iterator over the lines in the new version of file. |
700 | 633 |
701 The new version is the file in the user's workspace, i.e. the "right hand | 634 The new version is the file in the user's workspace, i.e. the "right hand |
702 side". | 635 side". |
703 | 636 |
704 Contents will be empty if the file is a directory or does not exist. | 637 Contents will be empty if the file is a directory or does not exist. |
705 Note: The carriage returns (LF or CR) are stripped off. | 638 Note: The carriage returns (LF or CR) are stripped off. |
706 """ | 639 """ |
707 if self._cached_new_contents is None: | 640 if self._cached_new_contents is None: |
708 self._cached_new_contents = [] | 641 self._cached_new_contents = [] |
709 if not self.IsDirectory(): | 642 try: |
710 try: | 643 self._cached_new_contents = gclient_utils.FileRead( |
711 self._cached_new_contents = gclient_utils.FileRead( | 644 self.AbsoluteLocalPath(), 'rU').splitlines() |
712 self.AbsoluteLocalPath(), 'rU').splitlines() | 645 except IOError: |
713 except IOError: | 646 pass # File not found? That's fine; maybe it was deleted. |
714 pass # File not found? That's fine; maybe it was deleted. | |
715 return self._cached_new_contents[:] | 647 return self._cached_new_contents[:] |
716 | 648 |
717 def ChangedContents(self): | 649 def ChangedContents(self): |
718 """Returns a list of tuples (line number, line text) of all new lines. | 650 """Returns a list of tuples (line number, line text) of all new lines. |
719 | 651 |
720 This relies on the scm diff output describing each changed code section | 652 This relies on the scm diff output describing each changed code section |
721 with a line of the form | 653 with a line of the form |
722 | 654 |
723 ^@@ <old line num>,<old size> <new line num>,<new size> @@$ | 655 ^@@ <old line num>,<old size> <new line num>,<new size> @@$ |
724 """ | 656 """ |
725 if self._cached_changed_contents is not None: | 657 if self._cached_changed_contents is not None: |
726 return self._cached_changed_contents[:] | 658 return self._cached_changed_contents[:] |
727 self._cached_changed_contents = [] | 659 self._cached_changed_contents = [] |
728 line_num = 0 | 660 line_num = 0 |
729 | 661 |
730 if self.IsDirectory(): | |
731 return [] | |
732 | |
733 for line in self.GenerateScmDiff().splitlines(): | 662 for line in self.GenerateScmDiff().splitlines(): |
734 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line) | 663 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line) |
735 if m: | 664 if m: |
736 line_num = int(m.groups(1)[0]) | 665 line_num = int(m.groups(1)[0]) |
737 continue | 666 continue |
738 if line.startswith('+') and not line.startswith('++'): | 667 if line.startswith('+') and not line.startswith('++'): |
739 self._cached_changed_contents.append((line_num, line[1:])) | 668 self._cached_changed_contents.append((line_num, line[1:])) |
740 if not line.startswith('-'): | 669 if not line.startswith('-'): |
741 line_num += 1 | 670 line_num += 1 |
742 return self._cached_changed_contents[:] | 671 return self._cached_changed_contents[:] |
743 | 672 |
744 def __str__(self): | 673 def __str__(self): |
745 return self.LocalPath() | 674 return self.LocalPath() |
746 | 675 |
747 def GenerateScmDiff(self): | 676 def GenerateScmDiff(self): |
748 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root) | 677 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root) |
749 | 678 |
750 | 679 |
751 class SvnAffectedFile(AffectedFile): | |
752 """Representation of a file in a change out of a Subversion checkout.""" | |
753 # Method 'NNN' is abstract in class 'NNN' but is not overridden | |
754 # pylint: disable=W0223 | |
755 | |
756 DIFF_CACHE = _SvnDiffCache | |
757 | |
758 def __init__(self, *args, **kwargs): | |
759 AffectedFile.__init__(self, *args, **kwargs) | |
760 self._server_path = None | |
761 self._is_text_file = None | |
762 | |
763 def ServerPath(self): | |
764 if self._server_path is None: | |
765 self._server_path = scm.SVN.CaptureLocalInfo( | |
766 [self.LocalPath()], self._local_root).get('URL', '') | |
767 return self._server_path | |
768 | |
769 def IsDirectory(self): | |
770 if self._is_directory is None: | |
771 path = self.AbsoluteLocalPath() | |
772 if os.path.exists(path): | |
773 # Retrieve directly from the file system; it is much faster than | |
774 # querying subversion, especially on Windows. | |
775 self._is_directory = os.path.isdir(path) | |
776 else: | |
777 self._is_directory = scm.SVN.CaptureLocalInfo( | |
778 [self.LocalPath()], self._local_root | |
779 ).get('Node Kind') in ('dir', 'directory') | |
780 return self._is_directory | |
781 | |
782 def Property(self, property_name): | |
783 if not property_name in self._properties: | |
784 self._properties[property_name] = scm.SVN.GetFileProperty( | |
785 self.LocalPath(), property_name, self._local_root).rstrip() | |
786 return self._properties[property_name] | |
787 | |
788 def IsTextFile(self): | |
789 if self._is_text_file is None: | |
790 if self.Action() == 'D': | |
791 # A deleted file is not a text file. | |
792 self._is_text_file = False | |
793 elif self.IsDirectory(): | |
794 self._is_text_file = False | |
795 else: | |
796 mime_type = scm.SVN.GetFileProperty( | |
797 self.LocalPath(), 'svn:mime-type', self._local_root) | |
798 self._is_text_file = (not mime_type or mime_type.startswith('text/')) | |
799 return self._is_text_file | |
800 | |
801 | |
802 class GitAffectedFile(AffectedFile): | 680 class GitAffectedFile(AffectedFile): |
803 """Representation of a file in a change out of a git checkout.""" | 681 """Representation of a file in a change out of a git checkout.""" |
804 # Method 'NNN' is abstract in class 'NNN' but is not overridden | 682 # Method 'NNN' is abstract in class 'NNN' but is not overridden |
805 # pylint: disable=W0223 | 683 # pylint: disable=W0223 |
806 | 684 |
807 DIFF_CACHE = _GitDiffCache | 685 DIFF_CACHE = _GitDiffCache |
808 | 686 |
809 def __init__(self, *args, **kwargs): | 687 def __init__(self, *args, **kwargs): |
810 AffectedFile.__init__(self, *args, **kwargs) | 688 AffectedFile.__init__(self, *args, **kwargs) |
811 self._server_path = None | 689 self._server_path = None |
812 self._is_text_file = None | 690 self._is_testable_file = None |
813 | 691 |
814 def ServerPath(self): | 692 def IsTestableFile(self): |
815 if self._server_path is None: | 693 if self._is_testable_file is None: |
816 raise NotImplementedError('TODO(maruel) Implement.') | 694 if self.Action() == 'D': |
817 return self._server_path | 695 # A deleted file is not testable. |
818 | 696 self._is_testable_file = False |
819 def IsDirectory(self): | |
820 if self._is_directory is None: | |
821 path = self.AbsoluteLocalPath() | |
822 if os.path.exists(path): | |
823 # Retrieve directly from the file system; it is much faster than | |
824 # querying subversion, especially on Windows. | |
825 self._is_directory = os.path.isdir(path) | |
826 else: | 697 else: |
827 self._is_directory = False | 698 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath()) |
828 return self._is_directory | 699 return self._is_testable_file |
829 | |
830 def Property(self, property_name): | |
831 if not property_name in self._properties: | |
832 raise NotImplementedError('TODO(maruel) Implement.') | |
833 return self._properties[property_name] | |
834 | |
835 def IsTextFile(self): | |
836 if self._is_text_file is None: | |
837 if self.Action() == 'D': | |
838 # A deleted file is not a text file. | |
839 self._is_text_file = False | |
840 elif self.IsDirectory(): | |
841 self._is_text_file = False | |
842 else: | |
843 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath()) | |
844 return self._is_text_file | |
845 | 700 |
846 | 701 |
847 class Change(object): | 702 class Change(object): |
848 """Describe a change. | 703 """Describe a change. |
849 | 704 |
850 Used directly by the presubmit scripts to query the current change being | 705 Used directly by the presubmit scripts to query the current change being |
851 tested. | 706 tested. |
852 | 707 |
853 Instance members: | 708 Instance members: |
854 tags: Dictionary of KEY=VALUE pairs found in the change description. | 709 tags: Dictionary of KEY=VALUE pairs found in the change description. |
(...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
936 def __getattr__(self, attr): | 791 def __getattr__(self, attr): |
937 """Return tags directly as attributes on the object.""" | 792 """Return tags directly as attributes on the object.""" |
938 if not re.match(r"^[A-Z_]*$", attr): | 793 if not re.match(r"^[A-Z_]*$", attr): |
939 raise AttributeError(self, attr) | 794 raise AttributeError(self, attr) |
940 return self.tags.get(attr) | 795 return self.tags.get(attr) |
941 | 796 |
942 def AllFiles(self, root=None): | 797 def AllFiles(self, root=None): |
943 """List all files under source control in the repo.""" | 798 """List all files under source control in the repo.""" |
944 raise NotImplementedError() | 799 raise NotImplementedError() |
945 | 800 |
946 def AffectedFiles(self, include_dirs=False, include_deletes=True, | 801 def AffectedFiles(self, include_deletes=True, file_filter=None): |
947 file_filter=None): | |
948 """Returns a list of AffectedFile instances for all files in the change. | 802 """Returns a list of AffectedFile instances for all files in the change. |
949 | 803 |
950 Args: | 804 Args: |
951 include_deletes: If false, deleted files will be filtered out. | 805 include_deletes: If false, deleted files will be filtered out. |
952 include_dirs: True to include directories in the list | |
953 file_filter: An additional filter to apply. | 806 file_filter: An additional filter to apply. |
954 | 807 |
955 Returns: | 808 Returns: |
956 [AffectedFile(path, action), AffectedFile(path, action)] | 809 [AffectedFile(path, action), AffectedFile(path, action)] |
957 """ | 810 """ |
958 if include_dirs: | 811 affected = filter(file_filter, self._affected_files) |
959 affected = self._affected_files | |
960 else: | |
961 affected = filter(lambda x: not x.IsDirectory(), self._affected_files) | |
962 | |
963 affected = filter(file_filter, affected) | |
964 | 812 |
965 if include_deletes: | 813 if include_deletes: |
966 return affected | 814 return affected |
967 else: | 815 else: |
968 return filter(lambda x: x.Action() != 'D', affected) | 816 return filter(lambda x: x.Action() != 'D', affected) |
969 | 817 |
970 def AffectedTextFiles(self, include_deletes=None): | 818 def AffectedTestableFiles(self, include_deletes=None): |
971 """Return a list of the existing text files in a change.""" | 819 """Return a list of the existing text files in a change.""" |
972 if include_deletes is not None: | 820 if include_deletes is not None: |
973 warn("AffectedTextFiles(include_deletes=%s)" | 821 warn("AffectedTeestableFiles(include_deletes=%s)" |
974 " is deprecated and ignored" % str(include_deletes), | 822 " is deprecated and ignored" % str(include_deletes), |
975 category=DeprecationWarning, | 823 category=DeprecationWarning, |
976 stacklevel=2) | 824 stacklevel=2) |
977 return filter(lambda x: x.IsTextFile(), | 825 return filter(lambda x: x.IsTestableFile(), |
978 self.AffectedFiles(include_dirs=False, include_deletes=False)) | 826 self.AffectedFiles(include_deletes=False)) |
979 | 827 |
980 def LocalPaths(self, include_dirs=False): | 828 def LocalPaths(self): |
981 """Convenience function.""" | 829 """Convenience function.""" |
982 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)] | 830 return [af.LocalPath() for af in self.AffectedFiles()] |
983 | 831 |
984 def AbsoluteLocalPaths(self, include_dirs=False): | 832 def AbsoluteLocalPaths(self): |
985 """Convenience function.""" | 833 """Convenience function.""" |
986 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)] | 834 return [af.AbsoluteLocalPath() for af in self.AffectedFiles()] |
987 | |
988 def ServerPaths(self, include_dirs=False): | |
989 """Convenience function.""" | |
990 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)] | |
991 | 835 |
992 def RightHandSideLines(self): | 836 def RightHandSideLines(self): |
993 """An iterator over all text lines in "new" version of changed files. | 837 """An iterator over all text lines in "new" version of changed files. |
994 | 838 |
995 Lists lines from new or modified text files in the change. | 839 Lists lines from new or modified text files in the change. |
996 | 840 |
997 This is useful for doing line-by-line regex checks, like checking for | 841 This is useful for doing line-by-line regex checks, like checking for |
998 trailing whitespace. | 842 trailing whitespace. |
999 | 843 |
1000 Yields: | 844 Yields: |
1001 a 3 tuple: | 845 a 3 tuple: |
1002 the AffectedFile instance of the current file; | 846 the AffectedFile instance of the current file; |
1003 integer line number (1-based); and | 847 integer line number (1-based); and |
1004 the contents of the line as a string. | 848 the contents of the line as a string. |
1005 """ | 849 """ |
1006 return _RightHandSideLinesImpl( | 850 return _RightHandSideLinesImpl( |
1007 x for x in self.AffectedFiles(include_deletes=False) | 851 x for x in self.AffectedFiles(include_deletes=False) |
1008 if x.IsTextFile()) | 852 if x.IsTestableFile()) |
1009 | |
1010 | |
1011 class SvnChange(Change): | |
1012 _AFFECTED_FILES = SvnAffectedFile | |
1013 scm = 'svn' | |
1014 _changelists = None | |
1015 | |
1016 def AllFiles(self, root=None): | |
1017 """List all files under source control in the repo.""" | |
1018 root = root or self.RepositoryRoot() | |
1019 return subprocess.check_output( | |
1020 ['svn', 'ls', '-R', '.'], cwd=root).splitlines() | |
1021 | 853 |
1022 | 854 |
1023 class GitChange(Change): | 855 class GitChange(Change): |
1024 _AFFECTED_FILES = GitAffectedFile | 856 _AFFECTED_FILES = GitAffectedFile |
1025 scm = 'git' | 857 scm = 'git' |
1026 | 858 |
1027 def AllFiles(self, root=None): | 859 def AllFiles(self, root=None): |
1028 """List all files under source control in the repo.""" | 860 """List all files under source control in the repo.""" |
1029 root = root or self.RepositoryRoot() | 861 root = root or self.RepositoryRoot() |
1030 return subprocess.check_output( | 862 return subprocess.check_output( |
(...skipping 473 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1504 os.environ = os.environ.copy() | 1336 os.environ = os.environ.copy() |
1505 os.environ['PYTHONDONTWRITEBYTECODE'] = '1' | 1337 os.environ['PYTHONDONTWRITEBYTECODE'] = '1' |
1506 | 1338 |
1507 output = PresubmitOutput(input_stream, output_stream) | 1339 output = PresubmitOutput(input_stream, output_stream) |
1508 if committing: | 1340 if committing: |
1509 output.write("Running presubmit commit checks ...\n") | 1341 output.write("Running presubmit commit checks ...\n") |
1510 else: | 1342 else: |
1511 output.write("Running presubmit upload checks ...\n") | 1343 output.write("Running presubmit upload checks ...\n") |
1512 start_time = time.time() | 1344 start_time = time.time() |
1513 presubmit_files = ListRelevantPresubmitFiles( | 1345 presubmit_files = ListRelevantPresubmitFiles( |
1514 change.AbsoluteLocalPaths(True), change.RepositoryRoot()) | 1346 change.AbsoluteLocalPaths(), change.RepositoryRoot()) |
1515 if not presubmit_files and verbose: | 1347 if not presubmit_files and verbose: |
1516 output.write("Warning, no PRESUBMIT.py found.\n") | 1348 output.write("Warning, no PRESUBMIT.py found.\n") |
1517 results = [] | 1349 results = [] |
1518 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose, | 1350 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose, |
1519 gerrit_obj, dry_run) | 1351 gerrit_obj, dry_run) |
1520 if default_presubmit: | 1352 if default_presubmit: |
1521 if verbose: | 1353 if verbose: |
1522 output.write("Running default presubmit script.\n") | 1354 output.write("Running default presubmit script.\n") |
1523 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py') | 1355 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py') |
1524 results += executer.ExecPresubmitScript(default_presubmit, fake_path) | 1356 results += executer.ExecPresubmitScript(default_presubmit, fake_path) |
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1596 def ParseFiles(args, recursive): | 1428 def ParseFiles(args, recursive): |
1597 logging.debug('Searching for %s', args) | 1429 logging.debug('Searching for %s', args) |
1598 files = [] | 1430 files = [] |
1599 for arg in args: | 1431 for arg in args: |
1600 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)]) | 1432 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)]) |
1601 return files | 1433 return files |
1602 | 1434 |
1603 | 1435 |
1604 def load_files(options, args): | 1436 def load_files(options, args): |
1605 """Tries to determine the SCM.""" | 1437 """Tries to determine the SCM.""" |
1606 change_scm = scm.determine_scm(options.root) | |
1607 files = [] | 1438 files = [] |
1608 if args: | 1439 if args: |
1609 files = ParseFiles(args, options.recursive) | 1440 files = ParseFiles(args, options.recursive) |
1610 if change_scm == 'svn': | 1441 change_scm = scm.determine_scm(options.root) |
1611 change_class = SvnChange | 1442 if change_scm == 'git': |
1612 if not files: | |
1613 files = scm.SVN.CaptureStatus([], options.root) | |
1614 elif change_scm == 'git': | |
1615 change_class = GitChange | 1443 change_class = GitChange |
1616 upstream = options.upstream or None | 1444 upstream = options.upstream or None |
1617 if not files: | 1445 if not files: |
1618 files = scm.GIT.CaptureStatus([], options.root, upstream) | 1446 files = scm.GIT.CaptureStatus([], options.root, upstream) |
1619 else: | 1447 else: |
1620 logging.info('Doesn\'t seem under source control. Got %d files', len(args)) | 1448 logging.info('Doesn\'t seem under source control. Got %d files', len(args)) |
1621 if not files: | 1449 if not files: |
1622 return None, None | 1450 return None, None |
1623 change_class = Change | 1451 change_class = Change |
1624 return change_class, files | 1452 return change_class, files |
(...skipping 180 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1805 return 2 | 1633 return 2 |
1806 | 1634 |
1807 | 1635 |
1808 if __name__ == '__main__': | 1636 if __name__ == '__main__': |
1809 fix_encoding.fix_encoding() | 1637 fix_encoding.fix_encoding() |
1810 try: | 1638 try: |
1811 sys.exit(main()) | 1639 sys.exit(main()) |
1812 except KeyboardInterrupt: | 1640 except KeyboardInterrupt: |
1813 sys.stderr.write('interrupted\n') | 1641 sys.stderr.write('interrupted\n') |
1814 sys.exit(2) | 1642 sys.exit(2) |
OLD | NEW |