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

Side by Side Diff: scm.py

Issue 1965001: Fix issue with svn copy/move directories. (Closed)
Patch Set: s/l/line/ Created 10 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | tests/scm_unittest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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)
OLDNEW
« no previous file with comments | « no previous file | tests/scm_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698