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, | |
|
Dan Beam
2016/11/29 19:13:45
can you grep for keywords when you remove them nex
Dan Beam
2016/11/29 19:16:29
"this broke" -> removing include_dirs=
| |
| 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): |
| 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)) |
| 449 | |
| 450 def AffectedTextFiles(self, include_deletes=None): | |
| 451 """An alias to AffectedTestableFiles for backwards compatibility.""" | |
| 452 return self.AffectedTestableFiles(include_deletes=include_deletes) | |
| 481 | 453 |
| 482 def FilterSourceFile(self, affected_file, white_list=None, black_list=None): | 454 def FilterSourceFile(self, affected_file, white_list=None, black_list=None): |
| 483 """Filters out files that aren't considered "source file". | 455 """Filters out files that aren't considered "source file". |
| 484 | 456 |
| 485 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST | 457 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST |
| 486 and InputApi.DEFAULT_BLACK_LIST is used respectively. | 458 and InputApi.DEFAULT_BLACK_LIST is used respectively. |
| 487 | 459 |
| 488 The lists will be compiled as regular expression and | 460 The lists will be compiled as regular expression and |
| 489 AffectedFile.LocalPath() needs to pass both list. | 461 AffectedFile.LocalPath() needs to pass both list. |
| 490 | 462 |
| 491 Note: Copy-paste this function to suit your needs or use a lambda function. | 463 Note: Copy-paste this function to suit your needs or use a lambda function. |
| 492 """ | 464 """ |
| 493 def Find(affected_file, items): | 465 def Find(affected_file, items): |
| 494 local_path = affected_file.LocalPath() | 466 local_path = affected_file.LocalPath() |
| 495 for item in items: | 467 for item in items: |
| 496 if self.re.match(item, local_path): | 468 if self.re.match(item, local_path): |
| 497 return True | 469 return True |
| 498 return False | 470 return False |
| 499 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and | 471 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and |
| 500 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST)) | 472 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST)) |
| 501 | 473 |
| 502 def AffectedSourceFiles(self, source_file): | 474 def AffectedSourceFiles(self, source_file): |
| 503 """Filter the list of AffectedTextFiles by the function source_file. | 475 """Filter the list of AffectedTestableFiles by the function source_file. |
| 504 | 476 |
| 505 If source_file is None, InputApi.FilterSourceFile() is used. | 477 If source_file is None, InputApi.FilterSourceFile() is used. |
| 506 """ | 478 """ |
| 507 if not source_file: | 479 if not source_file: |
| 508 source_file = self.FilterSourceFile | 480 source_file = self.FilterSourceFile |
| 509 return filter(source_file, self.AffectedTextFiles()) | 481 return filter(source_file, self.AffectedTestableFiles()) |
| 510 | 482 |
| 511 def RightHandSideLines(self, source_file_filter=None): | 483 def RightHandSideLines(self, source_file_filter=None): |
| 512 """An iterator over all text lines in "new" version of changed files. | 484 """An iterator over all text lines in "new" version of changed files. |
| 513 | 485 |
| 514 Only lists lines from new or modified text files in the change that are | 486 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. | 487 contained by the directory of the currently executing presubmit script. |
| 516 | 488 |
| 517 This is useful for doing line-by-line regex checks, like checking for | 489 This is useful for doing line-by-line regex checks, like checking for |
| 518 trailing whitespace. | 490 trailing whitespace. |
| 519 | 491 |
| (...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 572 """Caches diffs retrieved from a particular SCM.""" | 544 """Caches diffs retrieved from a particular SCM.""" |
| 573 def __init__(self, upstream=None): | 545 def __init__(self, upstream=None): |
| 574 """Stores the upstream revision against which all diffs will be computed.""" | 546 """Stores the upstream revision against which all diffs will be computed.""" |
| 575 self._upstream = upstream | 547 self._upstream = upstream |
| 576 | 548 |
| 577 def GetDiff(self, path, local_root): | 549 def GetDiff(self, path, local_root): |
| 578 """Get the diff for a particular path.""" | 550 """Get the diff for a particular path.""" |
| 579 raise NotImplementedError() | 551 raise NotImplementedError() |
| 580 | 552 |
| 581 | 553 |
| 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): | 554 class _GitDiffCache(_DiffCache): |
| 596 """DiffCache implementation for git; gets all file diffs at once.""" | 555 """DiffCache implementation for git; gets all file diffs at once.""" |
| 597 def __init__(self, upstream): | 556 def __init__(self, upstream): |
| 598 super(_GitDiffCache, self).__init__(upstream=upstream) | 557 super(_GitDiffCache, self).__init__(upstream=upstream) |
| 599 self._diffs_by_file = None | 558 self._diffs_by_file = None |
| 600 | 559 |
| 601 def GetDiff(self, path, local_root): | 560 def GetDiff(self, path, local_root): |
| 602 if not self._diffs_by_file: | 561 if not self._diffs_by_file: |
| 603 # Compute a single diff for all files and parse the output; should | 562 # 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. | 563 # 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 | 598 |
| 640 DIFF_CACHE = _DiffCache | 599 DIFF_CACHE = _DiffCache |
| 641 | 600 |
| 642 # Method could be a function | 601 # Method could be a function |
| 643 # pylint: disable=R0201 | 602 # pylint: disable=R0201 |
| 644 def __init__(self, path, action, repository_root, diff_cache): | 603 def __init__(self, path, action, repository_root, diff_cache): |
| 645 self._path = path | 604 self._path = path |
| 646 self._action = action | 605 self._action = action |
| 647 self._local_root = repository_root | 606 self._local_root = repository_root |
| 648 self._is_directory = None | 607 self._is_directory = None |
| 649 self._properties = {} | |
| 650 self._cached_changed_contents = None | 608 self._cached_changed_contents = None |
| 651 self._cached_new_contents = None | 609 self._cached_new_contents = None |
| 652 self._diff_cache = diff_cache | 610 self._diff_cache = diff_cache |
| 653 logging.debug('%s(%s)', self.__class__.__name__, self._path) | 611 logging.debug('%s(%s)', self.__class__.__name__, self._path) |
| 654 | 612 |
| 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): | 613 def LocalPath(self): |
| 663 """Returns the path of this file on the local disk relative to client root. | 614 """Returns the path of this file on the local disk relative to client root. |
| 664 """ | 615 """ |
| 665 return normpath(self._path) | 616 return normpath(self._path) |
| 666 | 617 |
| 667 def AbsoluteLocalPath(self): | 618 def AbsoluteLocalPath(self): |
| 668 """Returns the absolute path of this file on the local disk. | 619 """Returns the absolute path of this file on the local disk. |
| 669 """ | 620 """ |
| 670 return os.path.abspath(os.path.join(self._local_root, self.LocalPath())) | 621 return os.path.abspath(os.path.join(self._local_root, self.LocalPath())) |
| 671 | 622 |
| 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): | 623 def Action(self): |
| 681 """Returns the action on this opened file, e.g. A, M, D, etc.""" | 624 """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 | 625 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but |
| 683 # different for other SCM. | 626 # different for other SCM. |
| 684 return self._action | 627 return self._action |
| 685 | 628 |
| 686 def Property(self, property_name): | 629 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. | 630 """Returns True if the file is a text file and not a binary file. |
| 694 | 631 |
| 695 Deleted files are not text file.""" | 632 Deleted files are not text file.""" |
| 696 raise NotImplementedError() # Implement when needed | 633 raise NotImplementedError() # Implement when needed |
| 697 | 634 |
| 635 def IsTextFile(self): | |
| 636 """An alias to IsTestableFile for backwards compatibility.""" | |
| 637 return self.IsTestableFile() | |
| 638 | |
| 698 def NewContents(self): | 639 def NewContents(self): |
| 699 """Returns an iterator over the lines in the new version of file. | 640 """Returns an iterator over the lines in the new version of file. |
| 700 | 641 |
| 701 The new version is the file in the user's workspace, i.e. the "right hand | 642 The new version is the file in the user's workspace, i.e. the "right hand |
| 702 side". | 643 side". |
| 703 | 644 |
| 704 Contents will be empty if the file is a directory or does not exist. | 645 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. | 646 Note: The carriage returns (LF or CR) are stripped off. |
| 706 """ | 647 """ |
| 707 if self._cached_new_contents is None: | 648 if self._cached_new_contents is None: |
| 708 self._cached_new_contents = [] | 649 self._cached_new_contents = [] |
| 709 if not self.IsDirectory(): | 650 try: |
| 710 try: | 651 self._cached_new_contents = gclient_utils.FileRead( |
| 711 self._cached_new_contents = gclient_utils.FileRead( | 652 self.AbsoluteLocalPath(), 'rU').splitlines() |
| 712 self.AbsoluteLocalPath(), 'rU').splitlines() | 653 except IOError: |
| 713 except IOError: | 654 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[:] | 655 return self._cached_new_contents[:] |
| 716 | 656 |
| 717 def ChangedContents(self): | 657 def ChangedContents(self): |
| 718 """Returns a list of tuples (line number, line text) of all new lines. | 658 """Returns a list of tuples (line number, line text) of all new lines. |
| 719 | 659 |
| 720 This relies on the scm diff output describing each changed code section | 660 This relies on the scm diff output describing each changed code section |
| 721 with a line of the form | 661 with a line of the form |
| 722 | 662 |
| 723 ^@@ <old line num>,<old size> <new line num>,<new size> @@$ | 663 ^@@ <old line num>,<old size> <new line num>,<new size> @@$ |
| 724 """ | 664 """ |
| 725 if self._cached_changed_contents is not None: | 665 if self._cached_changed_contents is not None: |
| 726 return self._cached_changed_contents[:] | 666 return self._cached_changed_contents[:] |
| 727 self._cached_changed_contents = [] | 667 self._cached_changed_contents = [] |
| 728 line_num = 0 | 668 line_num = 0 |
| 729 | 669 |
| 730 if self.IsDirectory(): | |
| 731 return [] | |
| 732 | |
| 733 for line in self.GenerateScmDiff().splitlines(): | 670 for line in self.GenerateScmDiff().splitlines(): |
| 734 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line) | 671 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line) |
| 735 if m: | 672 if m: |
| 736 line_num = int(m.groups(1)[0]) | 673 line_num = int(m.groups(1)[0]) |
| 737 continue | 674 continue |
| 738 if line.startswith('+') and not line.startswith('++'): | 675 if line.startswith('+') and not line.startswith('++'): |
| 739 self._cached_changed_contents.append((line_num, line[1:])) | 676 self._cached_changed_contents.append((line_num, line[1:])) |
| 740 if not line.startswith('-'): | 677 if not line.startswith('-'): |
| 741 line_num += 1 | 678 line_num += 1 |
| 742 return self._cached_changed_contents[:] | 679 return self._cached_changed_contents[:] |
| 743 | 680 |
| 744 def __str__(self): | 681 def __str__(self): |
| 745 return self.LocalPath() | 682 return self.LocalPath() |
| 746 | 683 |
| 747 def GenerateScmDiff(self): | 684 def GenerateScmDiff(self): |
| 748 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root) | 685 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root) |
| 749 | 686 |
| 750 | 687 |
| 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): | 688 class GitAffectedFile(AffectedFile): |
| 803 """Representation of a file in a change out of a git checkout.""" | 689 """Representation of a file in a change out of a git checkout.""" |
| 804 # Method 'NNN' is abstract in class 'NNN' but is not overridden | 690 # Method 'NNN' is abstract in class 'NNN' but is not overridden |
| 805 # pylint: disable=W0223 | 691 # pylint: disable=W0223 |
| 806 | 692 |
| 807 DIFF_CACHE = _GitDiffCache | 693 DIFF_CACHE = _GitDiffCache |
| 808 | 694 |
| 809 def __init__(self, *args, **kwargs): | 695 def __init__(self, *args, **kwargs): |
| 810 AffectedFile.__init__(self, *args, **kwargs) | 696 AffectedFile.__init__(self, *args, **kwargs) |
| 811 self._server_path = None | 697 self._server_path = None |
| 812 self._is_text_file = None | 698 self._is_testable_file = None |
| 813 | 699 |
| 814 def ServerPath(self): | 700 def IsTestableFile(self): |
| 815 if self._server_path is None: | 701 if self._is_testable_file is None: |
| 816 raise NotImplementedError('TODO(maruel) Implement.') | 702 if self.Action() == 'D': |
| 817 return self._server_path | 703 # A deleted file is not testable. |
| 818 | 704 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: | 705 else: |
| 827 self._is_directory = False | 706 self._is_testable_file = os.path.isfile(self.AbsoluteLocalPath()) |
| 828 return self._is_directory | 707 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 | 708 |
| 846 | 709 |
| 847 class Change(object): | 710 class Change(object): |
| 848 """Describe a change. | 711 """Describe a change. |
| 849 | 712 |
| 850 Used directly by the presubmit scripts to query the current change being | 713 Used directly by the presubmit scripts to query the current change being |
| 851 tested. | 714 tested. |
| 852 | 715 |
| 853 Instance members: | 716 Instance members: |
| 854 tags: Dictionary of KEY=VALUE pairs found in the change description. | 717 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): | 799 def __getattr__(self, attr): |
| 937 """Return tags directly as attributes on the object.""" | 800 """Return tags directly as attributes on the object.""" |
| 938 if not re.match(r"^[A-Z_]*$", attr): | 801 if not re.match(r"^[A-Z_]*$", attr): |
| 939 raise AttributeError(self, attr) | 802 raise AttributeError(self, attr) |
| 940 return self.tags.get(attr) | 803 return self.tags.get(attr) |
| 941 | 804 |
| 942 def AllFiles(self, root=None): | 805 def AllFiles(self, root=None): |
| 943 """List all files under source control in the repo.""" | 806 """List all files under source control in the repo.""" |
| 944 raise NotImplementedError() | 807 raise NotImplementedError() |
| 945 | 808 |
| 946 def AffectedFiles(self, include_dirs=False, include_deletes=True, | 809 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. | 810 """Returns a list of AffectedFile instances for all files in the change. |
| 949 | 811 |
| 950 Args: | 812 Args: |
| 951 include_deletes: If false, deleted files will be filtered out. | 813 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. | 814 file_filter: An additional filter to apply. |
| 954 | 815 |
| 955 Returns: | 816 Returns: |
| 956 [AffectedFile(path, action), AffectedFile(path, action)] | 817 [AffectedFile(path, action), AffectedFile(path, action)] |
| 957 """ | 818 """ |
| 958 if include_dirs: | 819 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 | 820 |
| 965 if include_deletes: | 821 if include_deletes: |
| 966 return affected | 822 return affected |
| 967 else: | 823 else: |
| 968 return filter(lambda x: x.Action() != 'D', affected) | 824 return filter(lambda x: x.Action() != 'D', affected) |
| 969 | 825 |
| 970 def AffectedTextFiles(self, include_deletes=None): | 826 def AffectedTestableFiles(self, include_deletes=None): |
| 971 """Return a list of the existing text files in a change.""" | 827 """Return a list of the existing text files in a change.""" |
| 972 if include_deletes is not None: | 828 if include_deletes is not None: |
| 973 warn("AffectedTextFiles(include_deletes=%s)" | 829 warn("AffectedTeestableFiles(include_deletes=%s)" |
| 974 " is deprecated and ignored" % str(include_deletes), | 830 " is deprecated and ignored" % str(include_deletes), |
| 975 category=DeprecationWarning, | 831 category=DeprecationWarning, |
| 976 stacklevel=2) | 832 stacklevel=2) |
| 977 return filter(lambda x: x.IsTextFile(), | 833 return filter(lambda x: x.IsTestableFile(), |
| 978 self.AffectedFiles(include_dirs=False, include_deletes=False)) | 834 self.AffectedFiles(include_deletes=False)) |
| 979 | 835 |
| 980 def LocalPaths(self, include_dirs=False): | 836 def AffectedTextFiles(self, include_deletes=None): |
| 837 """An alias to AffectedTestableFiles for backwards compatibility.""" | |
| 838 return self.AffectedTestableFiles(include_deletes=include_deletes) | |
| 839 | |
| 840 def LocalPaths(self): | |
| 981 """Convenience function.""" | 841 """Convenience function.""" |
| 982 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)] | 842 return [af.LocalPath() for af in self.AffectedFiles()] |
| 983 | 843 |
| 984 def AbsoluteLocalPaths(self, include_dirs=False): | 844 def AbsoluteLocalPaths(self): |
| 985 """Convenience function.""" | 845 """Convenience function.""" |
| 986 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)] | 846 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 | 847 |
| 992 def RightHandSideLines(self): | 848 def RightHandSideLines(self): |
| 993 """An iterator over all text lines in "new" version of changed files. | 849 """An iterator over all text lines in "new" version of changed files. |
| 994 | 850 |
| 995 Lists lines from new or modified text files in the change. | 851 Lists lines from new or modified text files in the change. |
| 996 | 852 |
| 997 This is useful for doing line-by-line regex checks, like checking for | 853 This is useful for doing line-by-line regex checks, like checking for |
| 998 trailing whitespace. | 854 trailing whitespace. |
| 999 | 855 |
| 1000 Yields: | 856 Yields: |
| 1001 a 3 tuple: | 857 a 3 tuple: |
| 1002 the AffectedFile instance of the current file; | 858 the AffectedFile instance of the current file; |
| 1003 integer line number (1-based); and | 859 integer line number (1-based); and |
| 1004 the contents of the line as a string. | 860 the contents of the line as a string. |
| 1005 """ | 861 """ |
| 1006 return _RightHandSideLinesImpl( | 862 return _RightHandSideLinesImpl( |
| 1007 x for x in self.AffectedFiles(include_deletes=False) | 863 x for x in self.AffectedFiles(include_deletes=False) |
| 1008 if x.IsTextFile()) | 864 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 | 865 |
| 1022 | 866 |
| 1023 class GitChange(Change): | 867 class GitChange(Change): |
| 1024 _AFFECTED_FILES = GitAffectedFile | 868 _AFFECTED_FILES = GitAffectedFile |
| 1025 scm = 'git' | 869 scm = 'git' |
| 1026 | 870 |
| 1027 def AllFiles(self, root=None): | 871 def AllFiles(self, root=None): |
| 1028 """List all files under source control in the repo.""" | 872 """List all files under source control in the repo.""" |
| 1029 root = root or self.RepositoryRoot() | 873 root = root or self.RepositoryRoot() |
| 1030 return subprocess.check_output( | 874 return subprocess.check_output( |
| (...skipping 473 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1504 os.environ = os.environ.copy() | 1348 os.environ = os.environ.copy() |
| 1505 os.environ['PYTHONDONTWRITEBYTECODE'] = '1' | 1349 os.environ['PYTHONDONTWRITEBYTECODE'] = '1' |
| 1506 | 1350 |
| 1507 output = PresubmitOutput(input_stream, output_stream) | 1351 output = PresubmitOutput(input_stream, output_stream) |
| 1508 if committing: | 1352 if committing: |
| 1509 output.write("Running presubmit commit checks ...\n") | 1353 output.write("Running presubmit commit checks ...\n") |
| 1510 else: | 1354 else: |
| 1511 output.write("Running presubmit upload checks ...\n") | 1355 output.write("Running presubmit upload checks ...\n") |
| 1512 start_time = time.time() | 1356 start_time = time.time() |
| 1513 presubmit_files = ListRelevantPresubmitFiles( | 1357 presubmit_files = ListRelevantPresubmitFiles( |
| 1514 change.AbsoluteLocalPaths(True), change.RepositoryRoot()) | 1358 change.AbsoluteLocalPaths(), change.RepositoryRoot()) |
| 1515 if not presubmit_files and verbose: | 1359 if not presubmit_files and verbose: |
| 1516 output.write("Warning, no PRESUBMIT.py found.\n") | 1360 output.write("Warning, no PRESUBMIT.py found.\n") |
| 1517 results = [] | 1361 results = [] |
| 1518 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose, | 1362 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose, |
| 1519 gerrit_obj, dry_run) | 1363 gerrit_obj, dry_run) |
| 1520 if default_presubmit: | 1364 if default_presubmit: |
| 1521 if verbose: | 1365 if verbose: |
| 1522 output.write("Running default presubmit script.\n") | 1366 output.write("Running default presubmit script.\n") |
| 1523 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py') | 1367 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py') |
| 1524 results += executer.ExecPresubmitScript(default_presubmit, fake_path) | 1368 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): | 1440 def ParseFiles(args, recursive): |
| 1597 logging.debug('Searching for %s', args) | 1441 logging.debug('Searching for %s', args) |
| 1598 files = [] | 1442 files = [] |
| 1599 for arg in args: | 1443 for arg in args: |
| 1600 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)]) | 1444 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)]) |
| 1601 return files | 1445 return files |
| 1602 | 1446 |
| 1603 | 1447 |
| 1604 def load_files(options, args): | 1448 def load_files(options, args): |
| 1605 """Tries to determine the SCM.""" | 1449 """Tries to determine the SCM.""" |
| 1606 change_scm = scm.determine_scm(options.root) | |
| 1607 files = [] | 1450 files = [] |
| 1608 if args: | 1451 if args: |
| 1609 files = ParseFiles(args, options.recursive) | 1452 files = ParseFiles(args, options.recursive) |
| 1610 if change_scm == 'svn': | 1453 change_scm = scm.determine_scm(options.root) |
| 1611 change_class = SvnChange | 1454 if change_scm == 'git': |
| 1612 if not files: | |
| 1613 files = scm.SVN.CaptureStatus([], options.root) | |
| 1614 elif change_scm == 'git': | |
| 1615 change_class = GitChange | 1455 change_class = GitChange |
| 1616 upstream = options.upstream or None | 1456 upstream = options.upstream or None |
| 1617 if not files: | 1457 if not files: |
| 1618 files = scm.GIT.CaptureStatus([], options.root, upstream) | 1458 files = scm.GIT.CaptureStatus([], options.root, upstream) |
| 1619 else: | 1459 else: |
| 1620 logging.info('Doesn\'t seem under source control. Got %d files', len(args)) | 1460 logging.info('Doesn\'t seem under source control. Got %d files', len(args)) |
| 1621 if not files: | 1461 if not files: |
| 1622 return None, None | 1462 return None, None |
| 1623 change_class = Change | 1463 change_class = Change |
| 1624 return change_class, files | 1464 return change_class, files |
| (...skipping 180 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1805 return 2 | 1645 return 2 |
| 1806 | 1646 |
| 1807 | 1647 |
| 1808 if __name__ == '__main__': | 1648 if __name__ == '__main__': |
| 1809 fix_encoding.fix_encoding() | 1649 fix_encoding.fix_encoding() |
| 1810 try: | 1650 try: |
| 1811 sys.exit(main()) | 1651 sys.exit(main()) |
| 1812 except KeyboardInterrupt: | 1652 except KeyboardInterrupt: |
| 1813 sys.stderr.write('interrupted\n') | 1653 sys.stderr.write('interrupted\n') |
| 1814 sys.exit(2) | 1654 sys.exit(2) |
| OLD | NEW |