summaryrefslogtreecommitdiff
path: root/hgsubversion/stupid.py
diff options
context:
space:
mode:
Diffstat (limited to 'hgsubversion/stupid.py')
-rw-r--r--hgsubversion/stupid.py189
1 files changed, 136 insertions, 53 deletions
diff --git a/hgsubversion/stupid.py b/hgsubversion/stupid.py
index 159396e..cf39869 100644
--- a/hgsubversion/stupid.py
+++ b/hgsubversion/stupid.py
@@ -12,41 +12,114 @@ import svnwrap
import svnexternals
import util
+# Here is a diff mixing content and property changes in svn >= 1.7
+#
+# Index: a
+# ===================================================================
+# --- a (revision 12)
+# +++ a (working copy)
+# @@ -1,2 +1,3 @@
+# a
+# a
+# +a
+#
+# Property changes on: a
+# ___________________________________________________________________
+# Added: svn:executable
+# ## -0,0 +1 ##
+# +*
+
+class ParseError(Exception):
+ pass
-binary_file_re = re.compile(r'''Index: ([^\n]*)
+index_header = r'''Index: ([^\n]*)
=*
-Cannot display: file marked as a binary type.''')
-
-property_exec_set_re = re.compile(r'''Property changes on: ([^\n]*)
-_*
-(?:Added|Name): svn:executable
- \+''')
-
-property_exec_removed_re = re.compile(r'''Property changes on: ([^\n]*)
-_*
-(?:Deleted|Name): svn:executable
- -''')
-
-empty_file_patch_wont_make_re = re.compile(r'''Index: ([^\n]*)\n=*\n(?=Index:)''')
-
-any_file_re = re.compile(r'''^Index: ([^\n]*)\n=*\n''', re.MULTILINE)
-
-property_special_set_re = re.compile(r'''Property changes on: ([^\n]*)
-_*
-(?:Added|Name): svn:special
- \+''')
+'''
-property_special_removed_re = re.compile(r'''Property changes on: ([^\n]*)
+property_header = r'''Property changes on: ([^\n]*)
_*
-(?:Deleted|Name): svn:special
- \-''')
+'''
+
+headers_re = re.compile('(?:' + '|'.join([
+ index_header,
+ property_header,
+ ]) + ')')
+
+property_special_added = r'''(?:Added|Name): (svn:special)
+(?: \+|## -0,0 \+1 ##
+\+)'''
+
+property_special_deleted = r'''(?:Deleted|Name): (svn:special)
+(?: \-|## -1 \+0,0 ##
+\-)'''
+
+property_exec_added = r'''(?:Added|Name): (svn:executable)
+(?: \+|## -0,0 \+1 ##
+\+)'''
+
+property_exec_deleted = r'''(?:Deleted|Name): (svn:executable)
+(?: \-|## -1 \+0,0 ##
+\-)'''
+
+properties_re = re.compile('(?:' + '|'.join([
+ property_special_added,
+ property_special_deleted,
+ property_exec_added,
+ property_exec_deleted,
+ ]) + ')')
+
+class filediff:
+ def __init__(self, name):
+ self.name = name
+ self.diff = None
+ self.binary = False
+ self.executable = None
+ self.symlink = None
+ self.hasprops = False
+
+ def isempty(self):
+ return (not self.diff and not self.binary and not self.hasprops)
+
+ def maybedir(self):
+ return (not self.diff and not self.binary and self.hasprops
+ and self.symlink is None and self.executable is None)
+
+def parsediff(diff):
+ changes = {}
+ headers = headers_re.split(diff)[1:]
+ if (len(headers) % 3) != 0:
+ # headers should be a sequence of (index file, property file, data)
+ raise ParseError('unexpected diff format')
+ files = []
+ for i in xrange(len(headers)/3):
+ iname, pname, data = headers[3*i:3*i+3]
+ fname = iname or pname
+ if fname not in changes:
+ changes[fname] = filediff(fname)
+ files.append(changes[fname])
+ f = changes[fname]
+ if iname is not None:
+ if data.strip():
+ f.binary = data.lstrip().startswith(
+ 'Cannot display: file marked as a binary type.')
+ if not f.binary and '@@' in data:
+ # Non-empty diff
+ f.diff = data
+ else:
+ f.hasprops = True
+ for m in properties_re.finditer(data):
+ p = m.group(1, 2, 3, 4)
+ if p[0] or p[1]:
+ f.symlink = bool(p[0])
+ elif p[2] or p[3]:
+ f.executable = bool(p[2])
+ return files
class BadPatchApply(Exception):
pass
-
-def print_your_svn_is_old_message(ui): #pragma: no cover
+def print_your_svn_is_old_message(ui): # pragma: no cover
ui.status("In light of that, I'll fall back and do diffs, but it won't do "
"as good a job. You should really upgrade your server.\n")
@@ -215,17 +288,12 @@ def diff_branchrev(ui, svn, meta, branch, branchpath, r, parentctx):
if '\0' in d:
raise BadPatchApply('binary diffs are not supported')
files_data = {}
- # we have to pull each binary file by hand as a fulltext,
- # which sucks but we've got no choice
- binary_files = set(binary_file_re.findall(d))
- touched_files = set(binary_files)
- d2 = empty_file_patch_wont_make_re.sub('', d)
- d2 = property_exec_set_re.sub('', d2)
- d2 = property_exec_removed_re.sub('', d2)
+ changed = parsediff(d)
# Here we ensure that all files, including the new empty ones
# are marked as touched. Content is loaded on demand.
- touched_files.update(any_file_re.findall(d))
- if d2.strip() and len(re.findall('\n[-+]', d2.strip())) > 0:
+ touched_files = set(f.name for f in changed)
+ d2 = '\n'.join(f.diff for f in changed if f.diff)
+ if changed:
files_data = patchrepo(ui, meta, parentctx, cStringIO.StringIO(d2))
for x in files_data.iterkeys():
ui.note('M %s\n' % x)
@@ -233,23 +301,6 @@ def diff_branchrev(ui, svn, meta, branch, branchpath, r, parentctx):
ui.status('Not using patch for %s, diff had no hunks.\n' %
r.revnum)
- exec_files = {}
- for m in property_exec_removed_re.findall(d):
- exec_files[m] = False
- for m in property_exec_set_re.findall(d):
- exec_files[m] = True
- touched_files.update(exec_files)
- link_files = {}
- for m in property_special_set_re.findall(d):
- # TODO(augie) when a symlink is removed, patching will fail.
- # We're seeing that above - there's gotta be a better
- # workaround than just bailing like that.
- assert m in files_data
- link_files[m] = True
- for m in property_special_removed_re.findall(d):
- assert m in files_data
- link_files[m] = False
-
unknown_files = set()
for p in r.paths:
action = r.paths[p].action
@@ -273,9 +324,35 @@ def diff_branchrev(ui, svn, meta, branch, branchpath, r, parentctx):
touched_files.update(files_data)
touched_files.update(unknown_files)
+ # As of svn 1.7, diff may contain a lot of property changes for
+ # directories. We do not what to include these in our touched
+ # files list so we try to filter them while minimizing the number
+ # of svn API calls.
+ property_files = set(f.name for f in changed if f.maybedir())
+ property_files.discard('.')
+ touched_files.discard('.')
+ branchprefix = (branchpath and branchpath + '/') or branchpath
+ for f in list(property_files):
+ if f in parentctx:
+ continue
+ # We can be smarter here by checking if f is a subcomponent
+ # of a know path in parentctx or touched_files. KISS for now.
+ kind = svn.checkpath(branchprefix + f, r.revnum)
+ if kind == 'd':
+ touched_files.discard(f)
+
copies = getcopies(svn, meta, branch, branchpath, r, touched_files,
parentctx)
+ # We note binary files because svn's diff format doesn't describe
+ # what changed, only that a change occurred. This means we'll have
+ # to pull them as fulltexts from the server outside the diff
+ # apply.
+ binary_files = set(f.name for f in changed if f.binary)
+ exec_files = dict((f.name, f.executable) for f in changed
+ if f.executable is not None)
+ link_files = dict((f.name, f.symlink) for f in changed
+ if f.symlink is not None)
def filectxfn(repo, memctx, path):
if path in files_data and files_data[path] is None:
raise IOError(errno.ENOENT, '%s is deleted' % path)
@@ -638,7 +715,13 @@ def convert_rev(ui, meta, svn, r, tbdelta, firstrun):
deleted_branches[b] = parentctx.node()
continue
- incremental = (meta.revmap.oldest > 0)
+ # The nullrev check might not be necessary in theory but svn <
+ # 1.7 failed to diff branch creation so the diff_branchrev()
+ # path does not support this case with svn >= 1.7. We can fix
+ # it, or we can force the existing fetch_branchrev() path. Do
+ # the latter for now.
+ incremental = (meta.revmap.oldest > 0 and
+ parentctx.rev() != node.nullrev)
if incremental:
try: