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: |