Mercurial > hgsubversion
comparison hgsubversion/editor.py @ 937:fb6f6b7fa5a5
editor: implement file batons
The concept of current.file is incorrect, svn_delta.h documents open
file lifetime as:
* 5. When the producer calls @c open_file or @c add_file, either:
*
* (a) The producer must follow with any changes to the file
* (@c change_file_prop and/or @c apply_textdelta, as applicable),
* followed by a @c close_file call, before issuing any other file
* or directory calls, or
*
* (b) The producer must follow with a @c change_file_prop call if
* it is applicable, before issuing any other file or directory
* calls; later, after all directory batons including the root
* have been closed, the producer must issue @c apply_textdelta
* and @c close_file calls.
So, an open file can be kept open until after the root directory is
closed and have deltas applied afterwards. In the meantime, other files
may have been opened and patched, overwriting the current.file variable.
This patch fixes it by introducing file batons bound to file paths, and
using them to deduce the correct target in apply_textdelta(). In theory,
open files could be put in a staging area until they are closed and
moved in the RevisionData. But the current code registers files copied
during a directory copy as open files and these will not receive a
close_file() event. This separation will be enforced later.
author | Patrick Mezard <patrick@mezard.eu> |
---|---|
date | Sun, 23 Sep 2012 19:52:48 +0200 |
parents | 1de83496df4e |
children | f9014e28721b |
comparison
equal
deleted
inserted
replaced
936:bb599a47a9d0 | 937:fb6f6b7fa5a5 |
---|---|
33 def __init__(self, ui): | 33 def __init__(self, ui): |
34 self.ui = ui | 34 self.ui = ui |
35 self.clear() | 35 self.clear() |
36 | 36 |
37 def clear(self): | 37 def clear(self): |
38 self.file = None | |
39 self.added = set() | 38 self.added = set() |
40 self.files = {} | 39 self.files = {} |
41 self.deleted = {} | 40 self.deleted = {} |
42 self.rev = None | 41 self.rev = None |
43 self.execfiles = {} | 42 self.execfiles = {} |
100 self.set(p, data, 'x' in mode, 'l' in mode) | 99 self.set(p, data, 'x' in mode, 'l' in mode) |
101 | 100 |
102 self.missing = set() | 101 self.missing = set() |
103 self.ui.note('\n') | 102 self.ui.note('\n') |
104 | 103 |
104 class EditingError(Exception): | |
105 pass | |
105 | 106 |
106 class HgEditor(svnwrap.Editor): | 107 class HgEditor(svnwrap.Editor): |
107 | 108 |
108 def __init__(self, meta): | 109 def __init__(self, meta): |
109 self.meta = meta | 110 self.meta = meta |
110 self.ui = meta.ui | 111 self.ui = meta.ui |
111 self.repo = meta.repo | 112 self.repo = meta.repo |
112 self.current = RevisionData(meta.ui) | 113 self.current = RevisionData(meta.ui) |
114 self._clear() | |
115 | |
116 def _clear(self): | |
117 self._filecounter = 0 | |
118 self._filebatons = {} | |
119 self._files = {} | |
120 | |
121 def _addfilebaton(self, path): | |
122 # XXX: enforce unicity here. This cannot be done right now | |
123 # because copied files are mixed with open files and the cleanup | |
124 # rules in delete_entry() requires the distinction to be made to | |
125 # be implemented correctly. | |
126 self._filecounter += 1 | |
127 baton = '%d-%s' % (self._filecounter, path) | |
128 self._filebatons[baton] = path | |
129 self._files[path] = baton | |
130 return baton | |
113 | 131 |
114 @svnwrap.ieditor | 132 @svnwrap.ieditor |
115 def delete_entry(self, path, revision_bogus, parent_baton, pool=None): | 133 def delete_entry(self, path, revision_bogus, parent_baton, pool=None): |
116 br_path, branch = self.meta.split_branch_path(path)[:2] | 134 br_path, branch = self.meta.split_branch_path(path)[:2] |
117 if br_path == '': | 135 if br_path == '': |
128 br_path2 = '' | 146 br_path2 = '' |
129 if br_path != '': | 147 if br_path != '': |
130 br_path2 = br_path + '/' | 148 br_path2 = br_path + '/' |
131 # assuming it is a directory | 149 # assuming it is a directory |
132 self.current.externals[path] = None | 150 self.current.externals[path] = None |
133 map(self.current.delete, [pat for pat in self.current.files.iterkeys() | 151 prefix = path + '/' |
134 if pat.startswith(path + '/')]) | 152 map(self.current.delete, |
153 [pat for pat in self.current.files.iterkeys() | |
154 if pat.startswith(prefix)]) | |
135 for f in ctx.walk(util.PrefixMatch(br_path2)): | 155 for f in ctx.walk(util.PrefixMatch(br_path2)): |
136 f_p = '%s/%s' % (path, f[len(br_path2):]) | 156 f_p = '%s/%s' % (path, f[len(br_path2):]) |
137 if f_p not in self.current.files: | 157 if f_p not in self.current.files: |
138 self.current.delete(f_p) | 158 self.current.delete(f_p) |
159 # Remove copied but deleted files | |
160 for f in list(self._files): | |
161 if f.startswith(prefix): | |
162 del self._filebatons[self._files.pop(f)] | |
139 self.current.delete(path) | 163 self.current.delete(path) |
164 if path in self._files: | |
165 del self._filebatons[self._files.pop(path)] | |
140 | 166 |
141 @svnwrap.ieditor | 167 @svnwrap.ieditor |
142 def open_file(self, path, parent_baton, base_revision, p=None): | 168 def open_file(self, path, parent_baton, base_revision, p=None): |
143 self.current.file = None | |
144 fpath, branch = self.meta.split_branch_path(path)[:2] | 169 fpath, branch = self.meta.split_branch_path(path)[:2] |
145 if not fpath: | 170 if not fpath: |
146 self.ui.debug('WARNING: Opening non-existant file %s\n' % path) | 171 self.ui.debug('WARNING: Opening non-existant file %s\n' % path) |
147 return | 172 return None |
148 | 173 |
149 self.current.file = path | 174 if path in self._files: |
175 # Remove this when copied files are no longer registered as | |
176 # open files. | |
177 return self._files[path] | |
178 | |
150 self.ui.note('M %s\n' % path) | 179 self.ui.note('M %s\n' % path) |
151 | 180 |
152 if self.current.file in self.current.files: | |
153 return | |
154 | |
155 if not self.meta.is_path_valid(path): | 181 if not self.meta.is_path_valid(path): |
156 return | 182 return None |
157 | 183 |
158 baserev = base_revision | 184 baserev = base_revision |
159 if baserev is None or baserev == -1: | 185 if baserev is None or baserev == -1: |
160 baserev = self.current.rev.revnum - 1 | 186 baserev = self.current.rev.revnum - 1 |
161 # Use exact=True because during replacements ('R' action) we select | 187 # Use exact=True because during replacements ('R' action) we select |
163 # agains replaced branch. | 189 # agains replaced branch. |
164 parent = self.meta.get_parent_revision(baserev + 1, branch, True) | 190 parent = self.meta.get_parent_revision(baserev + 1, branch, True) |
165 ctx = self.repo[parent] | 191 ctx = self.repo[parent] |
166 if fpath not in ctx: | 192 if fpath not in ctx: |
167 self.current.missing.add(path) | 193 self.current.missing.add(path) |
168 return | 194 return None |
169 | 195 |
170 fctx = ctx.filectx(fpath) | 196 fctx = ctx.filectx(fpath) |
171 base = fctx.data() | 197 base = fctx.data() |
172 if 'l' in fctx.flags(): | 198 if 'l' in fctx.flags(): |
173 base = 'link ' + base | 199 base = 'link ' + base |
174 self.current.set(path, base, 'x' in fctx.flags(), 'l' in fctx.flags()) | 200 self.current.set(path, base, 'x' in fctx.flags(), 'l' in fctx.flags()) |
201 return self._addfilebaton(path) | |
175 | 202 |
176 @svnwrap.ieditor | 203 @svnwrap.ieditor |
177 def add_file(self, path, parent_baton=None, copyfrom_path=None, | 204 def add_file(self, path, parent_baton=None, copyfrom_path=None, |
178 copyfrom_revision=None, file_pool=None): | 205 copyfrom_revision=None, file_pool=None): |
179 self.current.file = None | |
180 if path in self.current.deleted: | 206 if path in self.current.deleted: |
181 del self.current.deleted[path] | 207 del self.current.deleted[path] |
182 fpath, branch = self.meta.split_branch_path(path, existing=False)[:2] | 208 fpath, branch = self.meta.split_branch_path(path, existing=False)[:2] |
183 if not fpath: | 209 if not fpath: |
184 return | 210 return None |
185 if (branch not in self.meta.branches and | 211 if (branch not in self.meta.branches and |
186 not self.meta.get_path_tag(self.meta.remotename(branch))): | 212 not self.meta.get_path_tag(self.meta.remotename(branch))): |
187 # we know this branch will exist now, because it has at least one file. Rock. | 213 # we know this branch will exist now, because it has at least one file. Rock. |
188 self.meta.branches[branch] = None, 0, self.current.rev.revnum | 214 self.meta.branches[branch] = None, 0, self.current.rev.revnum |
189 self.current.file = path | |
190 if not copyfrom_path: | 215 if not copyfrom_path: |
191 self.ui.note('A %s\n' % path) | 216 self.ui.note('A %s\n' % path) |
192 self.current.added.add(path) | 217 self.current.added.add(path) |
193 self.current.set(path, '', False, False) | 218 self.current.set(path, '', False, False) |
194 return | 219 return self._addfilebaton(path) |
195 self.ui.note('A+ %s\n' % path) | 220 self.ui.note('A+ %s\n' % path) |
196 (from_file, | 221 (from_file, |
197 from_branch) = self.meta.split_branch_path(copyfrom_path)[:2] | 222 from_branch) = self.meta.split_branch_path(copyfrom_path)[:2] |
198 if not from_file: | 223 if not from_file: |
199 self.current.missing.add(path) | 224 self.current.missing.add(path) |
200 return | 225 return None |
201 # Use exact=True because during replacements ('R' action) we select | 226 # Use exact=True because during replacements ('R' action) we select |
202 # replacing branch as parent, but svn delta editor provides delta | 227 # replacing branch as parent, but svn delta editor provides delta |
203 # agains replaced branch. | 228 # agains replaced branch. |
204 ha = self.meta.get_parent_revision(copyfrom_revision + 1, | 229 ha = self.meta.get_parent_revision(copyfrom_revision + 1, |
205 from_branch, True) | 230 from_branch, True) |
213 self.current.rev.revnum, branch) | 238 self.current.rev.revnum, branch) |
214 if parentid != revlog.nullid: | 239 if parentid != revlog.nullid: |
215 parentctx = self.repo.changectx(parentid) | 240 parentctx = self.repo.changectx(parentid) |
216 if util.issamefile(parentctx, ctx, from_file): | 241 if util.issamefile(parentctx, ctx, from_file): |
217 self.current.copies[path] = from_file | 242 self.current.copies[path] = from_file |
243 return self._addfilebaton(path) | |
218 | 244 |
219 @svnwrap.ieditor | 245 @svnwrap.ieditor |
220 def add_directory(self, path, parent_baton, copyfrom_path, | 246 def add_directory(self, path, parent_baton, copyfrom_path, |
221 copyfrom_revision, dir_pool=None): | 247 copyfrom_revision, dir_pool=None): |
222 self.current.batons[path] = path | 248 self.current.batons[path] = path |
262 if not f.startswith(frompath): | 288 if not f.startswith(frompath): |
263 continue | 289 continue |
264 fctx = fromctx.filectx(f) | 290 fctx = fromctx.filectx(f) |
265 dest = path + '/' + f[len(frompath):] | 291 dest = path + '/' + f[len(frompath):] |
266 self.current.set(dest, fctx.data(), 'x' in fctx.flags(), 'l' in fctx.flags()) | 292 self.current.set(dest, fctx.data(), 'x' in fctx.flags(), 'l' in fctx.flags()) |
293 # Put copied files with open files for now, they should | |
294 # really be separated to reduce resource usage. | |
295 self._addfilebaton(dest) | |
267 if dest in self.current.deleted: | 296 if dest in self.current.deleted: |
268 del self.current.deleted[dest] | 297 del self.current.deleted[dest] |
269 if branch == source_branch: | 298 if branch == source_branch: |
270 copies[dest] = f | 299 copies[dest] = f |
271 if copies: | 300 if copies: |
286 self.current.externals[dest] = v | 315 self.current.externals[dest] = v |
287 return path | 316 return path |
288 | 317 |
289 @svnwrap.ieditor | 318 @svnwrap.ieditor |
290 def change_file_prop(self, file_baton, name, value, pool=None): | 319 def change_file_prop(self, file_baton, name, value, pool=None): |
320 if file_baton is None: | |
321 return | |
322 path = self._filebatons[file_baton] | |
291 if name == 'svn:executable': | 323 if name == 'svn:executable': |
292 self.current.execfiles[self.current.file] = bool(value is not None) | 324 self.current.execfiles[path] = bool(value is not None) |
293 elif name == 'svn:special': | 325 elif name == 'svn:special': |
294 self.current.symlinks[self.current.file] = bool(value is not None) | 326 self.current.symlinks[path] = bool(value is not None) |
295 | 327 |
296 @svnwrap.ieditor | 328 @svnwrap.ieditor |
297 def change_dir_prop(self, dir_baton, name, value, pool=None): | 329 def change_dir_prop(self, dir_baton, name, value, pool=None): |
298 if dir_baton is None: | 330 if dir_baton is None: |
299 return | 331 return |
301 if name == 'svn:externals': | 333 if name == 'svn:externals': |
302 self.current.externals[path] = value | 334 self.current.externals[path] = value |
303 | 335 |
304 @svnwrap.ieditor | 336 @svnwrap.ieditor |
305 def open_root(self, edit_baton, base_revision, dir_pool=None): | 337 def open_root(self, edit_baton, base_revision, dir_pool=None): |
338 # We should not have to reset these, unfortunately the editor is | |
339 # reused for different revisions. | |
340 self._clear() | |
306 return None | 341 return None |
307 | 342 |
308 @svnwrap.ieditor | 343 @svnwrap.ieditor |
309 def open_directory(self, path, parent_baton, base_revision, dir_pool=None): | 344 def open_directory(self, path, parent_baton, base_revision, dir_pool=None): |
310 self.current.batons[path] = path | 345 self.current.batons[path] = path |
323 def apply_textdelta(self, file_baton, base_checksum, pool=None): | 358 def apply_textdelta(self, file_baton, base_checksum, pool=None): |
324 # We know coming in here the file must be one of the following options: | 359 # We know coming in here the file must be one of the following options: |
325 # 1) Deleted (invalid, fail an assertion) | 360 # 1) Deleted (invalid, fail an assertion) |
326 # 2) Missing a base text (bail quick since we have to fetch a full plaintext) | 361 # 2) Missing a base text (bail quick since we have to fetch a full plaintext) |
327 # 3) Has a base text in self.current.files, apply deltas | 362 # 3) Has a base text in self.current.files, apply deltas |
363 path = self._filebatons.get(file_baton) | |
364 if path is None or not self.meta.is_path_valid(path): | |
365 return lambda x: None | |
366 | |
328 base = '' | 367 base = '' |
329 if not self.meta.is_path_valid(self.current.file): | 368 if path in self.current.deleted: |
369 msg = ('cannot apply textdelta to %s: file is deleted' % path) | |
370 raise IOError(errno.ENOENT, msg) | |
371 | |
372 if (path not in self.current.files and | |
373 path not in self.current.missing): | |
374 msg = ('cannot apply textdelta to %s: file not found' % path) | |
375 raise IOError(errno.ENOENT, msg) | |
376 | |
377 if path in self.current.missing: | |
330 return lambda x: None | 378 return lambda x: None |
331 | 379 base = self.current.files[path] |
332 if self.current.file in self.current.deleted: | |
333 msg = ('cannot apply textdelta to %s: file is deleted' | |
334 % self.current.file) | |
335 raise IOError(errno.ENOENT, msg) | |
336 | |
337 if (self.current.file not in self.current.files and | |
338 self.current.file not in self.current.missing): | |
339 msg = ('cannot apply textdelta to %s: file not found' | |
340 % self.current.file) | |
341 raise IOError(errno.ENOENT, msg) | |
342 | |
343 if self.current.file in self.current.missing: | |
344 return lambda x: None | |
345 base = self.current.files[self.current.file] | |
346 target = NeverClosingStringIO() | 380 target = NeverClosingStringIO() |
347 self.stream = target | 381 self.stream = target |
348 | 382 |
349 handler = svnwrap.apply_txdelta(base, target) | 383 handler = svnwrap.apply_txdelta(base, target) |
350 if not callable(handler): # pragma: no cover | 384 if not callable(handler): # pragma: no cover |
351 raise hgutil.Abort('Error in Subversion bindings: ' | 385 raise hgutil.Abort('Error in Subversion bindings: ' |
352 'cannot call handler!') | 386 'cannot call handler!') |
353 def txdelt_window(window): | 387 def txdelt_window(window): |
354 try: | 388 try: |
355 if not self.meta.is_path_valid(self.current.file): | 389 if not self.meta.is_path_valid(path): |
356 return | 390 return |
357 try: | 391 try: |
358 handler(window) | 392 handler(window) |
359 except AssertionError, e: # pragma: no cover | 393 except AssertionError, e: # pragma: no cover |
360 # Enhance the exception message | 394 # Enhance the exception message |
367 e.args = (msg,) + others | 401 e.args = (msg,) + others |
368 raise e | 402 raise e |
369 | 403 |
370 # window being None means commit this file | 404 # window being None means commit this file |
371 if not window: | 405 if not window: |
372 self.current.files[self.current.file] = target.getvalue() | 406 self.current.files[path] = target.getvalue() |
373 except svnwrap.SubversionException, e: # pragma: no cover | 407 except svnwrap.SubversionException, e: # pragma: no cover |
374 if e.args[1] == svnwrap.ERR_INCOMPLETE_DATA: | 408 if e.args[1] == svnwrap.ERR_INCOMPLETE_DATA: |
375 self.current.missing.add(self.current.file) | 409 self.current.missing.add(path) |
376 else: # pragma: no cover | 410 else: # pragma: no cover |
377 raise hgutil.Abort(*e.args) | 411 raise hgutil.Abort(*e.args) |
378 except: # pragma: no cover | 412 except: # pragma: no cover |
379 self._exception_info = sys.exc_info() | 413 self._exception_info = sys.exc_info() |
380 raise | 414 raise |