Commit 3c55d981 authored by maruel@chromium.org's avatar maruel@chromium.org

Fix issue with svn copy/move directories.

svn diff would only generate a diff for modified files but the current code
would not respect full_move=True. full_move=True is necessary to make it work on
the try server.

TEST=svn move chrome chrome2; echo foo>>PRESUBMIT.py
The try job should fail but the diff should be right.
BUG=none

Review URL: http://codereview.chromium.org/1965001

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@46566 0039d316-1c4b-4281-b951-d872f2087c98
parent c3057686
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
"""SCM-specific utility classes.""" """SCM-specific utility classes."""
import cStringIO
import glob import glob
import os import os
import re import re
...@@ -41,6 +42,27 @@ def GetCasedPath(path): ...@@ -41,6 +42,27 @@ def GetCasedPath(path):
return 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): class GIT(object):
COMMAND = "git" COMMAND = "git"
...@@ -609,7 +631,11 @@ class SVN(object): ...@@ -609,7 +631,11 @@ class SVN(object):
@staticmethod @staticmethod
def IsMoved(filename): def IsMoved(filename):
"""Determine if a file has been added through svn mv""" """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 return (info.get('Copied From URL') and
info.get('Copied From Rev') and info.get('Copied From Rev') and
info.get('Schedule') == 'add') info.get('Schedule') == 'add')
...@@ -638,14 +664,11 @@ class SVN(object): ...@@ -638,14 +664,11 @@ class SVN(object):
def DiffItem(filename, full_move=False, revision=None): def DiffItem(filename, full_move=False, revision=None):
"""Diffs a single file. """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 Be sure to be in the appropriate directory before calling to have the
expected relative path. expected relative path.
full_move means that move or copy operations should completely recreate the 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.""" 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, # 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 # 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 # since we want the unified diff. Using --diff-cmd=diff doesn't always
...@@ -654,34 +677,61 @@ class SVN(object): ...@@ -654,34 +677,61 @@ class SVN(object):
# config directory, which gets around these problems. # config directory, which gets around these problems.
bogus_dir = tempfile.mkdtemp() bogus_dir = tempfile.mkdtemp()
try: try:
# Grabs the diff data. # Use "svn info" output instead of os.path.isdir because the latter fails
command = ["diff", "--config-dir", bogus_dir, filename] # when the file is deleted.
if revision: return SVN._DiffItemInternal(SVN.CaptureInfo(filename),
command.extend(['--revision', revision]) full_move=full_move, revision=revision)
if SVN.IsMoved(filename): finally:
if full_move: shutil.rmtree(bogus_dir)
file_content = gclient_utils.FileRead(filename, 'rb')
# Prepend '+' to every lines. @staticmethod
file_content = ['+' + i for i in file_content.splitlines(True)] def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
nb_lines = len(file_content) revision=None):
# We need to use / since patch on unix will fail otherwise. """Grabs the diff data."""
data = "Index: %s\n" % filename command = ["diff", "--config-dir", bogus_dir, filename]
data += '=' * 67 + '\n' if revision:
# Note: Should we use /dev/null instead? command.extend(['--revision', revision])
data += "--- %s\n" % filename data = None
data += "+++ %s\n" % filename if SVN.IsMovedInfo(info):
data += "@@ -0,0 +1,%d @@\n" % nb_lines if full_move:
data += ''.join(file_content) 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: 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. # svn diff on a mv/cp'd file outputs nothing if there was no change.
data = SVN.Capture(command, None) data = SVN.Capture(command, None)
if not data: if not data:
# We put in an empty Index entry so upload.py knows about them. # We put in an empty Index entry so upload.py knows about them.
data = "Index: %s\n" % filename 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) data = SVN.Capture(command, None)
finally: # Otherwise silently ignore directories.
shutil.rmtree(bogus_dir)
return data return data
@staticmethod @staticmethod
...@@ -701,17 +751,47 @@ class SVN(object): ...@@ -701,17 +751,47 @@ class SVN(object):
if os.path.normcase(path).startswith(root): if os.path.normcase(path).startswith(root):
return path[len(root):] return path[len(root):]
return path 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: try:
os.chdir(root) os.chdir(root)
diff = "".join(filter(None, # Cleanup filenames
[SVN.DiffItem(RelativePath(f, root), filenames = [RelativePath(f, root) for f in filenames]
full_move=full_move, # Get information about the modified items (files and directories)
revision=revision) data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
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: finally:
os.chdir(previous_cwd) os.chdir(previous_cwd)
return diff shutil.rmtree(bogus_dir)
@staticmethod @staticmethod
def GetEmail(repo_root): def GetEmail(repo_root):
......
...@@ -26,9 +26,9 @@ class RootTestCase(BaseSCMTestCase): ...@@ -26,9 +26,9 @@ class RootTestCase(BaseSCMTestCase):
def testMembersChanged(self): def testMembersChanged(self):
self.mox.ReplayAll() self.mox.ReplayAll()
members = [ members = [
'GetCasedPath', 'GIT', 'SVN', 'ValidateEmail', 'GetCasedPath', 'GenFakeDiff', 'GIT', 'SVN', 'ValidateEmail',
'gclient_utils', 'glob', 'os', 're', 'shutil', 'subprocess', 'sys', 'cStringIO', 'gclient_utils', 'glob', 'os', 're', 'shutil',
'tempfile', 'time', 'xml', 'subprocess', 'sys', 'tempfile', 'time', 'xml',
] ]
# If this test fails, you should add the relevant test. # If this test fails, you should add the relevant test.
self.compareMembers(scm, members) self.compareMembers(scm, members)
...@@ -149,9 +149,10 @@ class SVNTestCase(BaseSCMTestCase): ...@@ -149,9 +149,10 @@ class SVNTestCase(BaseSCMTestCase):
members = [ members = [
'COMMAND', 'AssertVersion', 'Capture', 'CaptureBaseRevision', 'COMMAND', 'AssertVersion', 'Capture', 'CaptureBaseRevision',
'CaptureHeadRevision', 'CaptureInfo', 'CaptureStatus', 'CaptureHeadRevision', 'CaptureInfo', 'CaptureStatus',
'current_version', 'DiffItem', 'GenerateDiff', 'GetCheckoutRoot', 'current_version', 'DiffItem', 'GenerateDiff',
'GetEmail', 'GetFileProperty', 'IsMoved', 'ReadSimpleAuth', 'Run', 'GetCheckoutRoot', 'GetEmail', 'GetFileProperty', 'IsMoved',
'RunAndFilterOutput', 'RunAndGetFileList', 'IsMovedInfo', 'ReadSimpleAuth', 'Run', 'RunAndFilterOutput',
'RunAndGetFileList',
] ]
# If this test fails, you should add the relevant test. # If this test fails, you should add the relevant test.
self.compareMembers(scm.SVN, members) self.compareMembers(scm.SVN, members)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment