Mercurial > hgsubversion
comparison hgsubversion/stupid.py @ 859:1d07e86f5797
stupid: handle changes in svn 1.7 diff format
Metadata changes are now represented like:
Property changes on: a
___________________________________________________________________
Added: svn:executable
## -0,0 +1 ##
+*
instead of:
Property changes on: a
___________________________________________________________________
Added: svn:executable
+ *
Also, I got tired of massaging the diff with regexps, so I extracted the
parsing logic in parsediff(). This is no small refactoring but it makes things
cleaner and the test suite pass on 1.6 and 1.7 so...
| author | Patrick Mezard <patrick@mezard.eu> |
|---|---|
| date | Thu, 19 Apr 2012 14:59:50 +0200 |
| parents | 805ef27fbcbe |
| children | 4cf20a687bff c24130e9ddb7 |
comparison
equal
deleted
inserted
replaced
| 858:bb6a013abaed | 859:1d07e86f5797 |
|---|---|
| 10 | 10 |
| 11 import svnwrap | 11 import svnwrap |
| 12 import svnexternals | 12 import svnexternals |
| 13 import util | 13 import util |
| 14 | 14 |
| 15 | 15 # Here is a diff mixing content and property changes in svn >= 1.7 |
| 16 binary_file_re = re.compile(r'''Index: ([^\n]*) | 16 # |
| 17 # Index: a | |
| 18 # =================================================================== | |
| 19 # --- a (revision 12) | |
| 20 # +++ a (working copy) | |
| 21 # @@ -1,2 +1,3 @@ | |
| 22 # a | |
| 23 # a | |
| 24 # +a | |
| 25 # | |
| 26 # Property changes on: a | |
| 27 # ___________________________________________________________________ | |
| 28 # Added: svn:executable | |
| 29 # ## -0,0 +1 ## | |
| 30 # +* | |
| 31 | |
| 32 class ParseError(Exception): | |
| 33 pass | |
| 34 | |
| 35 index_header = r'''Index: ([^\n]*) | |
| 17 =* | 36 =* |
| 18 Cannot display: file marked as a binary type.''') | 37 ''' |
| 19 | 38 |
| 20 property_exec_set_re = re.compile(r'''Property changes on: ([^\n]*) | 39 property_header = r'''Property changes on: ([^\n]*) |
| 21 _* | 40 _* |
| 22 (?:Added|Name): svn:executable | 41 ''' |
| 23 \+''') | 42 |
| 24 | 43 headers_re = re.compile('(?:' + '|'.join([ |
| 25 property_exec_removed_re = re.compile(r'''Property changes on: ([^\n]*) | 44 index_header, |
| 26 _* | 45 property_header, |
| 27 (?:Deleted|Name): svn:executable | 46 ]) + ')') |
| 28 -''') | 47 |
| 29 | 48 property_special_added = r'''(?:Added|Name): (svn:special) |
| 30 empty_file_patch_wont_make_re = re.compile(r'''Index: ([^\n]*)\n=*\n(?=Index:)''') | 49 (?: \+|## -0,0 \+1 ## |
| 31 | 50 \+)''' |
| 32 any_file_re = re.compile(r'''^Index: ([^\n]*)\n=*\n''', re.MULTILINE) | 51 |
| 33 | 52 property_special_deleted = r'''(?:Deleted|Name): (svn:special) |
| 34 property_special_set_re = re.compile(r'''Property changes on: ([^\n]*) | 53 (?: \-|## -1 \+0,0 ## |
| 35 _* | 54 \-)''' |
| 36 (?:Added|Name): svn:special | 55 |
| 37 \+''') | 56 property_exec_added = r'''(?:Added|Name): (svn:executable) |
| 38 | 57 (?: \+|## -0,0 \+1 ## |
| 39 property_special_removed_re = re.compile(r'''Property changes on: ([^\n]*) | 58 \+)''' |
| 40 _* | 59 |
| 41 (?:Deleted|Name): svn:special | 60 property_exec_deleted = r'''(?:Deleted|Name): (svn:executable) |
| 42 \-''') | 61 (?: \-|## -1 \+0,0 ## |
| 62 \-)''' | |
| 63 | |
| 64 properties_re = re.compile('(?:' + '|'.join([ | |
| 65 property_special_added, | |
| 66 property_special_deleted, | |
| 67 property_exec_added, | |
| 68 property_exec_deleted, | |
| 69 ]) + ')') | |
| 70 | |
| 71 class filediff: | |
| 72 def __init__(self, name): | |
| 73 self.name = name | |
| 74 self.diff = None | |
| 75 self.binary = False | |
| 76 self.executable = None | |
| 77 self.symlink = None | |
| 78 self.hasprops = False | |
| 79 | |
| 80 def isempty(self): | |
| 81 return (not self.diff and not self.binary and not self.hasprops) | |
| 82 | |
| 83 def parsediff(diff): | |
| 84 changes = {} | |
| 85 headers = headers_re.split(diff)[1:] | |
| 86 if (len(headers) % 3) != 0: | |
| 87 # headers should be a sequence of (index file, property file, data) | |
| 88 raise ParseError('unexpected diff format') | |
| 89 files = [] | |
| 90 for i in xrange(len(headers)/3): | |
| 91 iname, pname, data = headers[3*i:3*i+3] | |
| 92 fname = iname or pname | |
| 93 if fname not in changes: | |
| 94 changes[fname] = filediff(fname) | |
| 95 files.append(changes[fname]) | |
| 96 f = changes[fname] | |
| 97 if iname is not None: | |
| 98 if data.strip(): | |
| 99 f.binary = data.lstrip().startswith( | |
| 100 'Cannot display: file marked as a binary type.') | |
| 101 if not f.binary and '@@' in data: | |
| 102 # Non-empty diff | |
| 103 f.diff = data | |
| 104 else: | |
| 105 f.hasprops = True | |
| 106 for m in properties_re.finditer(data): | |
| 107 p = m.group(1, 2, 3, 4) | |
| 108 if p[0] or p[1]: | |
| 109 f.symlink = bool(p[0]) | |
| 110 elif p[2] or p[3]: | |
| 111 f.executable = bool(p[2]) | |
| 112 return files | |
| 43 | 113 |
| 44 | 114 |
| 45 class BadPatchApply(Exception): | 115 class BadPatchApply(Exception): |
| 46 pass | 116 pass |
| 47 | |
| 48 | 117 |
| 49 def print_your_svn_is_old_message(ui): # pragma: no cover | 118 def print_your_svn_is_old_message(ui): # pragma: no cover |
| 50 ui.status("In light of that, I'll fall back and do diffs, but it won't do " | 119 ui.status("In light of that, I'll fall back and do diffs, but it won't do " |
| 51 "as good a job. You should really upgrade your server.\n") | 120 "as good a job. You should really upgrade your server.\n") |
| 52 | 121 |
| 213 raise | 282 raise |
| 214 raise BadPatchApply('previous revision does not exist') | 283 raise BadPatchApply('previous revision does not exist') |
| 215 if '\0' in d: | 284 if '\0' in d: |
| 216 raise BadPatchApply('binary diffs are not supported') | 285 raise BadPatchApply('binary diffs are not supported') |
| 217 files_data = {} | 286 files_data = {} |
| 218 # we have to pull each binary file by hand as a fulltext, | 287 changed = parsediff(d) |
| 219 # which sucks but we've got no choice | |
| 220 binary_files = set(binary_file_re.findall(d)) | |
| 221 touched_files = set(binary_files) | |
| 222 d2 = empty_file_patch_wont_make_re.sub('', d) | |
| 223 d2 = property_exec_set_re.sub('', d2) | |
| 224 d2 = property_exec_removed_re.sub('', d2) | |
| 225 # Here we ensure that all files, including the new empty ones | 288 # Here we ensure that all files, including the new empty ones |
| 226 # are marked as touched. Content is loaded on demand. | 289 # are marked as touched. Content is loaded on demand. |
| 227 touched_files.update(any_file_re.findall(d)) | 290 touched_files = set(f.name for f in changed) |
| 228 if d2.strip() and len(re.findall('\n[-+]', d2.strip())) > 0: | 291 d2 = '\n'.join(f.diff for f in changed if f.diff) |
| 292 if changed: | |
| 229 files_data = patchrepo(ui, meta, parentctx, cStringIO.StringIO(d2)) | 293 files_data = patchrepo(ui, meta, parentctx, cStringIO.StringIO(d2)) |
| 230 for x in files_data.iterkeys(): | 294 for x in files_data.iterkeys(): |
| 231 ui.note('M %s\n' % x) | 295 ui.note('M %s\n' % x) |
| 232 else: | 296 else: |
| 233 ui.status('Not using patch for %s, diff had no hunks.\n' % | 297 ui.status('Not using patch for %s, diff had no hunks.\n' % |
| 234 r.revnum) | 298 r.revnum) |
| 235 | 299 |
| 236 exec_files = {} | |
| 237 for m in property_exec_removed_re.findall(d): | |
| 238 exec_files[m] = False | |
| 239 for m in property_exec_set_re.findall(d): | |
| 240 exec_files[m] = True | |
| 241 touched_files.update(exec_files) | |
| 242 link_files = {} | |
| 243 for m in property_special_set_re.findall(d): | |
| 244 # TODO(augie) when a symlink is removed, patching will fail. | |
| 245 # We're seeing that above - there's gotta be a better | |
| 246 # workaround than just bailing like that. | |
| 247 assert m in files_data | |
| 248 link_files[m] = True | |
| 249 for m in property_special_removed_re.findall(d): | |
| 250 assert m in files_data | |
| 251 link_files[m] = False | |
| 252 | |
| 253 unknown_files = set() | 300 unknown_files = set() |
| 254 for p in r.paths: | 301 for p in r.paths: |
| 255 action = r.paths[p].action | 302 action = r.paths[p].action |
| 256 if not p.startswith(branchpath) or action not in 'DR': | 303 if not p.startswith(branchpath) or action not in 'DR': |
| 257 continue | 304 continue |
| 274 touched_files.update(unknown_files) | 321 touched_files.update(unknown_files) |
| 275 | 322 |
| 276 copies = getcopies(svn, meta, branch, branchpath, r, touched_files, | 323 copies = getcopies(svn, meta, branch, branchpath, r, touched_files, |
| 277 parentctx) | 324 parentctx) |
| 278 | 325 |
| 326 binary_files = set(f.name for f in changed if f.binary) | |
| 327 exec_files = dict((f.name, f.executable) for f in changed | |
| 328 if f.executable is not None) | |
| 329 link_files = dict((f.name, f.symlink) for f in changed | |
| 330 if f.symlink is not None) | |
| 279 def filectxfn(repo, memctx, path): | 331 def filectxfn(repo, memctx, path): |
| 280 if path in files_data and files_data[path] is None: | 332 if path in files_data and files_data[path] is None: |
| 281 raise IOError(errno.ENOENT, '%s is deleted' % path) | 333 raise IOError(errno.ENOENT, '%s is deleted' % path) |
| 282 | 334 |
| 283 if path in binary_files or path in unknown_files: | 335 if path in binary_files or path in unknown_files: |
