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.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 |