Index: scm.py |
diff --git a/scm.py b/scm.py |
index 038d2dd4719a5ad777c383e6a83fc346eae9a9da..ac3f03fce0d5e2f8d296f3cd49ed4a963866fcc6 100644 |
--- a/scm.py |
+++ b/scm.py |
@@ -4,6 +4,7 @@ |
"""SCM-specific utility classes.""" |
+import cStringIO |
import glob |
import os |
import re |
@@ -41,6 +42,27 @@ def GetCasedPath(path): |
return path |
+def GenFakeDiff(filename): |
+ """Generates a fake diff from a file.""" |
+ file_content = gclient_utils.FileRead(filename, 'rb').splitlines(True) |
+ nb_lines = len(file_content) |
+ # We need to use / since patch on unix will fail otherwise. |
+ data = cStringIO.StringIO() |
+ data.write("Index: %s\n" % filename) |
+ data.write('=' * 67 + '\n') |
+ # Note: Should we use /dev/null instead? |
+ data.write("--- %s\n" % filename) |
+ data.write("+++ %s\n" % filename) |
+ data.write("@@ -0,0 +1,%d @@\n" % nb_lines) |
+ # Prepend '+' to every lines. |
+ for line in file_content: |
+ data.write('+') |
+ data.write(line) |
+ result = data.getvalue() |
+ data.close() |
+ return result |
+ |
+ |
class GIT(object): |
COMMAND = "git" |
@@ -609,7 +631,11 @@ class SVN(object): |
@staticmethod |
def IsMoved(filename): |
"""Determine if a file has been added through svn mv""" |
- info = SVN.CaptureInfo(filename) |
+ return SVN.IsMovedInfo(SVN.CaptureInfo(filename)) |
+ |
+ @staticmethod |
+ def IsMovedInfo(info): |
+ """Determine if a file has been added through svn mv""" |
return (info.get('Copied From URL') and |
info.get('Copied From Rev') and |
info.get('Schedule') == 'add') |
@@ -638,14 +664,11 @@ class SVN(object): |
def DiffItem(filename, full_move=False, revision=None): |
"""Diffs a single file. |
+ Should be simple, eh? No it isn't. |
Be sure to be in the appropriate directory before calling to have the |
expected relative path. |
full_move means that move or copy operations should completely recreate the |
files, usually in the prospect to apply the patch for a try job.""" |
- # Use svn info output instead of os.path.isdir because the latter fails |
- # when the file is deleted. |
- if SVN.CaptureInfo(filename).get("Node Kind") == "directory": |
- return None |
# If the user specified a custom diff command in their svn config file, |
# then it'll be used when we do svn diff, which we don't want to happen |
# since we want the unified diff. Using --diff-cmd=diff doesn't always |
@@ -654,34 +677,61 @@ class SVN(object): |
# config directory, which gets around these problems. |
bogus_dir = tempfile.mkdtemp() |
try: |
- # Grabs the diff data. |
- command = ["diff", "--config-dir", bogus_dir, filename] |
- if revision: |
- command.extend(['--revision', revision]) |
- if SVN.IsMoved(filename): |
- if full_move: |
- file_content = gclient_utils.FileRead(filename, 'rb') |
- # Prepend '+' to every lines. |
- file_content = ['+' + i for i in file_content.splitlines(True)] |
- nb_lines = len(file_content) |
- # We need to use / since patch on unix will fail otherwise. |
- data = "Index: %s\n" % filename |
- data += '=' * 67 + '\n' |
- # Note: Should we use /dev/null instead? |
- data += "--- %s\n" % filename |
- data += "+++ %s\n" % filename |
- data += "@@ -0,0 +1,%d @@\n" % nb_lines |
- data += ''.join(file_content) |
+ # Use "svn info" output instead of os.path.isdir because the latter fails |
+ # when the file is deleted. |
+ return SVN._DiffItemInternal(SVN.CaptureInfo(filename), |
+ full_move=full_move, revision=revision) |
+ finally: |
+ shutil.rmtree(bogus_dir) |
+ |
+ @staticmethod |
+ def _DiffItemInternal(filename, info, bogus_dir, full_move=False, |
+ revision=None): |
+ """Grabs the diff data.""" |
+ command = ["diff", "--config-dir", bogus_dir, filename] |
+ if revision: |
+ command.extend(['--revision', revision]) |
+ data = None |
+ if SVN.IsMovedInfo(info): |
+ if full_move: |
+ if info.get("Node Kind") == "directory": |
+ # Things become tricky here. It's a directory copy/move. We need to |
+ # diff all the files inside it. |
+ # This will put a lot of pressure on the heap. This is why StringIO |
+ # is used and converted back into a string at the end. The reason to |
+ # return a string instead of a StringIO is that StringIO.write() |
+ # doesn't accept a StringIO object. *sigh*. |
+ for (dirpath, dirnames, filenames) in os.walk(filename): |
+ # Cleanup all files starting with a '.'. |
+ for d in dirnames: |
+ if d.startswith('.'): |
+ dirnames.remove(d) |
+ for f in filenames: |
+ if f.startswith('.'): |
+ filenames.remove(f) |
+ for f in filenames: |
+ if data is None: |
+ data = cStringIO.StringIO() |
+ data.write(GenFakeDiff(os.path.join(dirpath, f))) |
+ if data: |
+ tmp = data.getvalue() |
+ data.close() |
+ data = tmp |
else: |
+ data = GenFakeDiff(filename) |
+ else: |
+ if info.get("Node Kind") != "directory": |
# svn diff on a mv/cp'd file outputs nothing if there was no change. |
data = SVN.Capture(command, None) |
if not data: |
# We put in an empty Index entry so upload.py knows about them. |
data = "Index: %s\n" % filename |
- else: |
+ # Otherwise silently ignore directories. |
+ else: |
+ if info.get("Node Kind") != "directory": |
+ # Normal simple case. |
data = SVN.Capture(command, None) |
- finally: |
- shutil.rmtree(bogus_dir) |
+ # Otherwise silently ignore directories. |
return data |
@staticmethod |
@@ -701,17 +751,47 @@ class SVN(object): |
if os.path.normcase(path).startswith(root): |
return path[len(root):] |
return path |
+ # If the user specified a custom diff command in their svn config file, |
+ # then it'll be used when we do svn diff, which we don't want to happen |
+ # since we want the unified diff. Using --diff-cmd=diff doesn't always |
+ # work, since they can have another diff executable in their path that |
+ # gives different line endings. So we use a bogus temp directory as the |
+ # config directory, which gets around these problems. |
+ bogus_dir = tempfile.mkdtemp() |
try: |
os.chdir(root) |
- diff = "".join(filter(None, |
- [SVN.DiffItem(RelativePath(f, root), |
- full_move=full_move, |
- revision=revision) |
- for f in filenames])) |
+ # Cleanup filenames |
+ filenames = [RelativePath(f, root) for f in filenames] |
+ # Get information about the modified items (files and directories) |
+ data = dict([(f, SVN.CaptureInfo(f)) for f in filenames]) |
+ if full_move: |
+ # Eliminate modified files inside moved/copied directory. |
+ for (filename, info) in data.iteritems(): |
+ if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory": |
+ # Remove files inside the directory. |
+ filenames = [f for f in filenames |
+ if not f.startswith(filename + os.path.sep)] |
+ for filename in data.keys(): |
+ if not filename in filenames: |
+ # Remove filtered out items. |
+ del data[filename] |
+ # Now ready to do the actual diff. |
+ diffs = [] |
+ for filename in sorted(data.iterkeys()): |
+ diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir, |
+ full_move=full_move, |
+ revision=revision)) |
+ # Use StringIO since it can be messy when diffing a directory move with |
+ # full_move=True. |
+ buf = cStringIO.StringIO() |
+ for d in filter(None, diffs): |
+ buf.write(d) |
+ result = buf.getvalue() |
+ buf.close() |
+ return result |
finally: |
os.chdir(previous_cwd) |
- return diff |
- |
+ shutil.rmtree(bogus_dir) |
@staticmethod |
def GetEmail(repo_root): |