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: