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