OLD | NEW |
1 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """SCM-specific utility classes.""" | 5 """SCM-specific utility classes.""" |
6 | 6 |
| 7 import cStringIO |
7 import glob | 8 import glob |
8 import os | 9 import os |
9 import re | 10 import re |
10 import shutil | 11 import shutil |
11 import subprocess | 12 import subprocess |
12 import sys | 13 import sys |
13 import tempfile | 14 import tempfile |
14 import time | 15 import time |
15 import xml.dom.minidom | 16 import xml.dom.minidom |
16 | 17 |
(...skipping 17 matching lines...) Expand all Loading... |
34 subpath = '\\'.join(paths[:i+1]) | 35 subpath = '\\'.join(paths[:i+1]) |
35 prev = len('\\'.join(paths[:i])) | 36 prev = len('\\'.join(paths[:i])) |
36 # glob.glob will return the cased path for the last item only. This is why | 37 # glob.glob will return the cased path for the last item only. This is why |
37 # we are calling it in a loop. Extract the data we want and put it back | 38 # we are calling it in a loop. Extract the data we want and put it back |
38 # into the list. | 39 # into the list. |
39 paths[i] = glob.glob(subpath + '*')[0][prev+1:len(subpath)] | 40 paths[i] = glob.glob(subpath + '*')[0][prev+1:len(subpath)] |
40 path = '\\'.join(paths) | 41 path = '\\'.join(paths) |
41 return path | 42 return path |
42 | 43 |
43 | 44 |
| 45 def GenFakeDiff(filename): |
| 46 """Generates a fake diff from a file.""" |
| 47 file_content = gclient_utils.FileRead(filename, 'rb').splitlines(True) |
| 48 nb_lines = len(file_content) |
| 49 # We need to use / since patch on unix will fail otherwise. |
| 50 data = cStringIO.StringIO() |
| 51 data.write("Index: %s\n" % filename) |
| 52 data.write('=' * 67 + '\n') |
| 53 # Note: Should we use /dev/null instead? |
| 54 data.write("--- %s\n" % filename) |
| 55 data.write("+++ %s\n" % filename) |
| 56 data.write("@@ -0,0 +1,%d @@\n" % nb_lines) |
| 57 # Prepend '+' to every lines. |
| 58 for line in file_content: |
| 59 data.write('+') |
| 60 data.write(line) |
| 61 result = data.getvalue() |
| 62 data.close() |
| 63 return result |
| 64 |
| 65 |
44 class GIT(object): | 66 class GIT(object): |
45 COMMAND = "git" | 67 COMMAND = "git" |
46 | 68 |
47 @staticmethod | 69 @staticmethod |
48 def Capture(args, in_directory=None, print_error=True, error_ok=False): | 70 def Capture(args, in_directory=None, print_error=True, error_ok=False): |
49 """Runs git, capturing output sent to stdout as a string. | 71 """Runs git, capturing output sent to stdout as a string. |
50 | 72 |
51 Args: | 73 Args: |
52 args: A sequence of command line parameters to be passed to git. | 74 args: A sequence of command line parameters to be passed to git. |
53 in_directory: The directory where git is to be run. | 75 in_directory: The directory where git is to be run. |
(...skipping 548 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
602 if wc_status[0].getAttribute('switched') == 'true': | 624 if wc_status[0].getAttribute('switched') == 'true': |
603 statuses[4] = 'S' | 625 statuses[4] = 'S' |
604 # TODO(maruel): Col 5 and 6 | 626 # TODO(maruel): Col 5 and 6 |
605 item = (''.join(statuses), file_path) | 627 item = (''.join(statuses), file_path) |
606 results.append(item) | 628 results.append(item) |
607 return results | 629 return results |
608 | 630 |
609 @staticmethod | 631 @staticmethod |
610 def IsMoved(filename): | 632 def IsMoved(filename): |
611 """Determine if a file has been added through svn mv""" | 633 """Determine if a file has been added through svn mv""" |
612 info = SVN.CaptureInfo(filename) | 634 return SVN.IsMovedInfo(SVN.CaptureInfo(filename)) |
| 635 |
| 636 @staticmethod |
| 637 def IsMovedInfo(info): |
| 638 """Determine if a file has been added through svn mv""" |
613 return (info.get('Copied From URL') and | 639 return (info.get('Copied From URL') and |
614 info.get('Copied From Rev') and | 640 info.get('Copied From Rev') and |
615 info.get('Schedule') == 'add') | 641 info.get('Schedule') == 'add') |
616 | 642 |
617 @staticmethod | 643 @staticmethod |
618 def GetFileProperty(file, property_name): | 644 def GetFileProperty(file, property_name): |
619 """Returns the value of an SVN property for the given file. | 645 """Returns the value of an SVN property for the given file. |
620 | 646 |
621 Args: | 647 Args: |
622 file: The file to check | 648 file: The file to check |
623 property_name: The name of the SVN property, e.g. "svn:mime-type" | 649 property_name: The name of the SVN property, e.g. "svn:mime-type" |
624 | 650 |
625 Returns: | 651 Returns: |
626 The value of the property, which will be the empty string if the property | 652 The value of the property, which will be the empty string if the property |
627 is not set on the file. If the file is not under version control, the | 653 is not set on the file. If the file is not under version control, the |
628 empty string is also returned. | 654 empty string is also returned. |
629 """ | 655 """ |
630 output = SVN.Capture(["propget", property_name, file]) | 656 output = SVN.Capture(["propget", property_name, file]) |
631 if (output.startswith("svn: ") and | 657 if (output.startswith("svn: ") and |
632 output.endswith("is not under version control")): | 658 output.endswith("is not under version control")): |
633 return "" | 659 return "" |
634 else: | 660 else: |
635 return output | 661 return output |
636 | 662 |
637 @staticmethod | 663 @staticmethod |
638 def DiffItem(filename, full_move=False, revision=None): | 664 def DiffItem(filename, full_move=False, revision=None): |
639 """Diffs a single file. | 665 """Diffs a single file. |
640 | 666 |
| 667 Should be simple, eh? No it isn't. |
641 Be sure to be in the appropriate directory before calling to have the | 668 Be sure to be in the appropriate directory before calling to have the |
642 expected relative path. | 669 expected relative path. |
643 full_move means that move or copy operations should completely recreate the | 670 full_move means that move or copy operations should completely recreate the |
644 files, usually in the prospect to apply the patch for a try job.""" | 671 files, usually in the prospect to apply the patch for a try job.""" |
645 # Use svn info output instead of os.path.isdir because the latter fails | |
646 # when the file is deleted. | |
647 if SVN.CaptureInfo(filename).get("Node Kind") == "directory": | |
648 return None | |
649 # If the user specified a custom diff command in their svn config file, | 672 # If the user specified a custom diff command in their svn config file, |
650 # then it'll be used when we do svn diff, which we don't want to happen | 673 # then it'll be used when we do svn diff, which we don't want to happen |
651 # since we want the unified diff. Using --diff-cmd=diff doesn't always | 674 # since we want the unified diff. Using --diff-cmd=diff doesn't always |
652 # work, since they can have another diff executable in their path that | 675 # work, since they can have another diff executable in their path that |
653 # gives different line endings. So we use a bogus temp directory as the | 676 # gives different line endings. So we use a bogus temp directory as the |
654 # config directory, which gets around these problems. | 677 # config directory, which gets around these problems. |
655 bogus_dir = tempfile.mkdtemp() | 678 bogus_dir = tempfile.mkdtemp() |
656 try: | 679 try: |
657 # Grabs the diff data. | 680 # Use "svn info" output instead of os.path.isdir because the latter fails |
658 command = ["diff", "--config-dir", bogus_dir, filename] | 681 # when the file is deleted. |
659 if revision: | 682 return SVN._DiffItemInternal(SVN.CaptureInfo(filename), |
660 command.extend(['--revision', revision]) | 683 full_move=full_move, revision=revision) |
661 if SVN.IsMoved(filename): | 684 finally: |
662 if full_move: | 685 shutil.rmtree(bogus_dir) |
663 file_content = gclient_utils.FileRead(filename, 'rb') | 686 |
664 # Prepend '+' to every lines. | 687 @staticmethod |
665 file_content = ['+' + i for i in file_content.splitlines(True)] | 688 def _DiffItemInternal(filename, info, bogus_dir, full_move=False, |
666 nb_lines = len(file_content) | 689 revision=None): |
667 # We need to use / since patch on unix will fail otherwise. | 690 """Grabs the diff data.""" |
668 data = "Index: %s\n" % filename | 691 command = ["diff", "--config-dir", bogus_dir, filename] |
669 data += '=' * 67 + '\n' | 692 if revision: |
670 # Note: Should we use /dev/null instead? | 693 command.extend(['--revision', revision]) |
671 data += "--- %s\n" % filename | 694 data = None |
672 data += "+++ %s\n" % filename | 695 if SVN.IsMovedInfo(info): |
673 data += "@@ -0,0 +1,%d @@\n" % nb_lines | 696 if full_move: |
674 data += ''.join(file_content) | 697 if info.get("Node Kind") == "directory": |
| 698 # Things become tricky here. It's a directory copy/move. We need to |
| 699 # diff all the files inside it. |
| 700 # This will put a lot of pressure on the heap. This is why StringIO |
| 701 # is used and converted back into a string at the end. The reason to |
| 702 # return a string instead of a StringIO is that StringIO.write() |
| 703 # doesn't accept a StringIO object. *sigh*. |
| 704 for (dirpath, dirnames, filenames) in os.walk(filename): |
| 705 # Cleanup all files starting with a '.'. |
| 706 for d in dirnames: |
| 707 if d.startswith('.'): |
| 708 dirnames.remove(d) |
| 709 for f in filenames: |
| 710 if f.startswith('.'): |
| 711 filenames.remove(f) |
| 712 for f in filenames: |
| 713 if data is None: |
| 714 data = cStringIO.StringIO() |
| 715 data.write(GenFakeDiff(os.path.join(dirpath, f))) |
| 716 if data: |
| 717 tmp = data.getvalue() |
| 718 data.close() |
| 719 data = tmp |
675 else: | 720 else: |
| 721 data = GenFakeDiff(filename) |
| 722 else: |
| 723 if info.get("Node Kind") != "directory": |
676 # svn diff on a mv/cp'd file outputs nothing if there was no change. | 724 # svn diff on a mv/cp'd file outputs nothing if there was no change. |
677 data = SVN.Capture(command, None) | 725 data = SVN.Capture(command, None) |
678 if not data: | 726 if not data: |
679 # We put in an empty Index entry so upload.py knows about them. | 727 # We put in an empty Index entry so upload.py knows about them. |
680 data = "Index: %s\n" % filename | 728 data = "Index: %s\n" % filename |
681 else: | 729 # Otherwise silently ignore directories. |
| 730 else: |
| 731 if info.get("Node Kind") != "directory": |
| 732 # Normal simple case. |
682 data = SVN.Capture(command, None) | 733 data = SVN.Capture(command, None) |
683 finally: | 734 # Otherwise silently ignore directories. |
684 shutil.rmtree(bogus_dir) | |
685 return data | 735 return data |
686 | 736 |
687 @staticmethod | 737 @staticmethod |
688 def GenerateDiff(filenames, root=None, full_move=False, revision=None): | 738 def GenerateDiff(filenames, root=None, full_move=False, revision=None): |
689 """Returns a string containing the diff for the given file list. | 739 """Returns a string containing the diff for the given file list. |
690 | 740 |
691 The files in the list should either be absolute paths or relative to the | 741 The files in the list should either be absolute paths or relative to the |
692 given root. If no root directory is provided, the repository root will be | 742 given root. If no root directory is provided, the repository root will be |
693 used. | 743 used. |
694 The diff will always use relative paths. | 744 The diff will always use relative paths. |
695 """ | 745 """ |
696 previous_cwd = os.getcwd() | 746 previous_cwd = os.getcwd() |
697 root = root or SVN.GetCheckoutRoot(previous_cwd) | 747 root = root or SVN.GetCheckoutRoot(previous_cwd) |
698 root = os.path.normcase(os.path.join(root, '')) | 748 root = os.path.normcase(os.path.join(root, '')) |
699 def RelativePath(path, root): | 749 def RelativePath(path, root): |
700 """We must use relative paths.""" | 750 """We must use relative paths.""" |
701 if os.path.normcase(path).startswith(root): | 751 if os.path.normcase(path).startswith(root): |
702 return path[len(root):] | 752 return path[len(root):] |
703 return path | 753 return path |
| 754 # If the user specified a custom diff command in their svn config file, |
| 755 # then it'll be used when we do svn diff, which we don't want to happen |
| 756 # since we want the unified diff. Using --diff-cmd=diff doesn't always |
| 757 # work, since they can have another diff executable in their path that |
| 758 # gives different line endings. So we use a bogus temp directory as the |
| 759 # config directory, which gets around these problems. |
| 760 bogus_dir = tempfile.mkdtemp() |
704 try: | 761 try: |
705 os.chdir(root) | 762 os.chdir(root) |
706 diff = "".join(filter(None, | 763 # Cleanup filenames |
707 [SVN.DiffItem(RelativePath(f, root), | 764 filenames = [RelativePath(f, root) for f in filenames] |
708 full_move=full_move, | 765 # Get information about the modified items (files and directories) |
709 revision=revision) | 766 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames]) |
710 for f in filenames])) | 767 if full_move: |
| 768 # Eliminate modified files inside moved/copied directory. |
| 769 for (filename, info) in data.iteritems(): |
| 770 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory": |
| 771 # Remove files inside the directory. |
| 772 filenames = [f for f in filenames |
| 773 if not f.startswith(filename + os.path.sep)] |
| 774 for filename in data.keys(): |
| 775 if not filename in filenames: |
| 776 # Remove filtered out items. |
| 777 del data[filename] |
| 778 # Now ready to do the actual diff. |
| 779 diffs = [] |
| 780 for filename in sorted(data.iterkeys()): |
| 781 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir, |
| 782 full_move=full_move, |
| 783 revision=revision)) |
| 784 # Use StringIO since it can be messy when diffing a directory move with |
| 785 # full_move=True. |
| 786 buf = cStringIO.StringIO() |
| 787 for d in filter(None, diffs): |
| 788 buf.write(d) |
| 789 result = buf.getvalue() |
| 790 buf.close() |
| 791 return result |
711 finally: | 792 finally: |
712 os.chdir(previous_cwd) | 793 os.chdir(previous_cwd) |
713 return diff | 794 shutil.rmtree(bogus_dir) |
714 | |
715 | 795 |
716 @staticmethod | 796 @staticmethod |
717 def GetEmail(repo_root): | 797 def GetEmail(repo_root): |
718 """Retrieves the svn account which we assume is an email address.""" | 798 """Retrieves the svn account which we assume is an email address.""" |
719 infos = SVN.CaptureInfo(repo_root) | 799 infos = SVN.CaptureInfo(repo_root) |
720 uuid = infos.get('UUID') | 800 uuid = infos.get('UUID') |
721 root = infos.get('Repository Root') | 801 root = infos.get('Repository Root') |
722 if not root: | 802 if not root: |
723 return None | 803 return None |
724 | 804 |
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
799 if not SVN.current_version: | 879 if not SVN.current_version: |
800 SVN.current_version = SVN.Capture(['--version']).split()[2] | 880 SVN.current_version = SVN.Capture(['--version']).split()[2] |
801 current_version_list = map(only_int, SVN.current_version.split('.')) | 881 current_version_list = map(only_int, SVN.current_version.split('.')) |
802 for min_ver in map(int, min_version.split('.')): | 882 for min_ver in map(int, min_version.split('.')): |
803 ver = current_version_list.pop(0) | 883 ver = current_version_list.pop(0) |
804 if ver < min_ver: | 884 if ver < min_ver: |
805 return (False, SVN.current_version) | 885 return (False, SVN.current_version) |
806 elif ver > min_ver: | 886 elif ver > min_ver: |
807 return (True, SVN.current_version) | 887 return (True, SVN.current_version) |
808 return (True, SVN.current_version) | 888 return (True, SVN.current_version) |
OLD | NEW |