Merge lp:~abentley/bzr/disk-transform into lp:~bzr/bzr/trunk-old

Proposed by Aaron Bentley on 2009-05-15
Status: Merged
Merged at revision: not available
Proposed branch: lp:~abentley/bzr/disk-transform
Merge into: lp:~bzr/bzr/trunk-old
Diff against target: 776 lines
To merge this branch: bzr merge lp:~abentley/bzr/disk-transform
Reviewer Review Type Date Requested Status
Ian Clatworthy 2009-05-15 Approve on 2009-05-26
Review via email: mp+6635@code.launchpad.net
To post a comment you must log in.
Aaron Bentley (abentley) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Hi all,

This patch refactors BaseTreeTranform by moving all code that assumes
limbo is disk-based into DiskTreeTransform, and by moving all methods
overridden by PreviewTreeTranform into TreeTransform.

This clears the way to implement a new TreeTransform type that doesn't
hit disk.

Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iEYEARECAAYFAkoN2ooACgkQ0F+nu1YWqI2cIwCfY0H3XpMLGRt5cIc7gXd6y4wC
GYkAnioLorhD672kUIbHAwgOMH454shp
=p07S
-----END PGP SIGNATURE-----

Ian Clatworthy (ian-clatworthy) wrote :

All looks good to me. Two minor things:

1. Please tweak the docstring for TreeTransformBase so it doesn't claim to be a base class for itself. :-)

2. You're changing the constructor interface for TreeTransformBase which is technically public (but practically private). I think we should therefore mention this refactoring in the Internals sections of NEWS.

review: Approve
lp:~abentley/bzr/disk-transform updated on 2009-05-28
4357. By Aaron Bentley on 2009-05-28

Merge bzr.dev into disk-transform.

4358. By Aaron Bentley on 2009-05-28

Update docs

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2009-05-23 04:55:52 +0000
3+++ NEWS 2009-05-28 08:35:37 +0000
4@@ -34,6 +34,10 @@
5
6 * Added osutils.parent_directories(). (Ian Clatworthy)
7
8+* TreeTransformBase no longer assumes that limbo is provided via disk.
9+ DiskTreeTransform now provides disk functionality. (Aaron Bentley)
10+
11+
12 Internals
13 *********
14
15
16=== modified file 'bzrlib/transform.py'
17--- bzrlib/transform.py 2009-05-23 04:55:52 +0000
18+++ bzrlib/transform.py 2009-05-28 08:35:37 +0000
19@@ -76,24 +76,20 @@
20
21
22 class TreeTransformBase(object):
23- """The base class for TreeTransform and TreeTransformBase"""
24+ """The base class for TreeTransform and its kin."""
25
26- def __init__(self, tree, limbodir, pb=DummyProgress(),
27+ def __init__(self, tree, pb=DummyProgress(),
28 case_sensitive=True):
29 """Constructor.
30
31 :param tree: The tree that will be transformed, but not necessarily
32 the output tree.
33- :param limbodir: A directory where new files can be stored until
34- they are installed in their proper places
35 :param pb: A ProgressBar indicating how much progress is being made
36 :param case_sensitive: If True, the target of the transform is
37 case sensitive, not just case preserving.
38 """
39 object.__init__(self)
40 self._tree = tree
41- self._limbodir = limbodir
42- self._deletiondir = None
43 self._id_number = 0
44 # mapping of trans_id -> new basename
45 self._new_name = {}
46@@ -101,15 +97,6 @@
47 self._new_parent = {}
48 # mapping of trans_id with new contents -> new file_kind
49 self._new_contents = {}
50- # A mapping of transform ids to their limbo filename
51- self._limbo_files = {}
52- # A mapping of transform ids to a set of the transform ids of children
53- # that their limbo directory has
54- self._limbo_children = {}
55- # Map transform ids to maps of child filename to child transform id
56- self._limbo_children_names = {}
57- # List of transform ids that need to be renamed from limbo into place
58- self._needs_rename = set()
59 # Set of trans_ids whose contents will be removed
60 self._removed_contents = set()
61 # Mapping of trans_id -> new execute-bit value
62@@ -128,10 +115,6 @@
63 self._tree_path_ids = {}
64 # Mapping trans_id -> path in old tree
65 self._tree_id_paths = {}
66- # Cache of realpath results, to speed up canonical_path
67- self._realpaths = {}
68- # Cache of relpath results, to speed up canonical_path
69- self._relpaths = {}
70 # The trans_id that will be used as the tree root
71 root_id = tree.get_root_id()
72 if root_id is not None:
73@@ -147,42 +130,22 @@
74 # A counter of how many files have been renamed
75 self.rename_count = 0
76
77+ def finalize(self):
78+ """Release the working tree lock, if held.
79+
80+ This is required if apply has not been invoked, but can be invoked
81+ even after apply.
82+ """
83+ if self._tree is None:
84+ return
85+ self._tree.unlock()
86+ self._tree = None
87+
88 def __get_root(self):
89 return self._new_root
90
91 root = property(__get_root)
92
93- def finalize(self):
94- """Release the working tree lock, if held, clean up limbo dir.
95-
96- This is required if apply has not been invoked, but can be invoked
97- even after apply.
98- """
99- if self._tree is None:
100- return
101- try:
102- entries = [(self._limbo_name(t), t, k) for t, k in
103- self._new_contents.iteritems()]
104- entries.sort(reverse=True)
105- for path, trans_id, kind in entries:
106- if kind == "directory":
107- os.rmdir(path)
108- else:
109- os.unlink(path)
110- try:
111- os.rmdir(self._limbodir)
112- except OSError:
113- # We don't especially care *why* the dir is immortal.
114- raise ImmortalLimbo(self._limbodir)
115- try:
116- if self._deletiondir is not None:
117- os.rmdir(self._deletiondir)
118- except OSError:
119- raise errors.ImmortalPendingDeletion(self._deletiondir)
120- finally:
121- self._tree.unlock()
122- self._tree = None
123-
124 def _assign_id(self):
125 """Produce a new tranform id"""
126 new_id = "new-%s" % self._id_number
127@@ -200,37 +163,12 @@
128 """Change the path that is assigned to a transaction id."""
129 if trans_id == self._new_root:
130 raise CantMoveRoot
131- previous_parent = self._new_parent.get(trans_id)
132- previous_name = self._new_name.get(trans_id)
133 self._new_name[trans_id] = name
134 self._new_parent[trans_id] = parent
135 if parent == ROOT_PARENT:
136 if self._new_root is not None:
137 raise ValueError("Cannot have multiple roots.")
138 self._new_root = trans_id
139- if (trans_id in self._limbo_files and
140- trans_id not in self._needs_rename):
141- self._rename_in_limbo([trans_id])
142- self._limbo_children[previous_parent].remove(trans_id)
143- del self._limbo_children_names[previous_parent][previous_name]
144-
145- def _rename_in_limbo(self, trans_ids):
146- """Fix limbo names so that the right final path is produced.
147-
148- This means we outsmarted ourselves-- we tried to avoid renaming
149- these files later by creating them with their final names in their
150- final parents. But now the previous name or parent is no longer
151- suitable, so we have to rename them.
152-
153- Even for trans_ids that have no new contents, we must remove their
154- entries from _limbo_files, because they are now stale.
155- """
156- for trans_id in trans_ids:
157- old_path = self._limbo_files.pop(trans_id)
158- if trans_id not in self._new_contents:
159- continue
160- new_path = self._limbo_name(trans_id)
161- os.rename(old_path, new_path)
162
163 def adjust_root_path(self, name, parent):
164 """Emulate moving the root by moving all children, instead.
165@@ -298,25 +236,6 @@
166 else:
167 return self.trans_id_tree_file_id(file_id)
168
169- def canonical_path(self, path):
170- """Get the canonical tree-relative path"""
171- # don't follow final symlinks
172- abs = self._tree.abspath(path)
173- if abs in self._relpaths:
174- return self._relpaths[abs]
175- dirname, basename = os.path.split(abs)
176- if dirname not in self._realpaths:
177- self._realpaths[dirname] = os.path.realpath(dirname)
178- dirname = self._realpaths[dirname]
179- abs = pathjoin(dirname, basename)
180- if dirname in self._relpaths:
181- relpath = pathjoin(self._relpaths[dirname], basename)
182- relpath = relpath.rstrip('/\\')
183- else:
184- relpath = self._tree.relpath(abs)
185- self._relpaths[abs] = relpath
186- return relpath
187-
188 def trans_id_tree_path(self, path):
189 """Determine (and maybe set) the transaction ID for a tree path."""
190 path = self.canonical_path(path)
191@@ -332,113 +251,6 @@
192 return ROOT_PARENT
193 return self.trans_id_tree_path(os.path.dirname(path))
194
195- def create_file(self, contents, trans_id, mode_id=None):
196- """Schedule creation of a new file.
197-
198- See also new_file.
199-
200- Contents is an iterator of strings, all of which will be written
201- to the target destination.
202-
203- New file takes the permissions of any existing file with that id,
204- unless mode_id is specified.
205- """
206- name = self._limbo_name(trans_id)
207- f = open(name, 'wb')
208- try:
209- try:
210- unique_add(self._new_contents, trans_id, 'file')
211- except:
212- # Clean up the file, it never got registered so
213- # TreeTransform.finalize() won't clean it up.
214- f.close()
215- os.unlink(name)
216- raise
217-
218- f.writelines(contents)
219- finally:
220- f.close()
221- self._set_mode(trans_id, mode_id, S_ISREG)
222-
223- def _set_mode(self, trans_id, mode_id, typefunc):
224- """Set the mode of new file contents.
225- The mode_id is the existing file to get the mode from (often the same
226- as trans_id). The operation is only performed if there's a mode match
227- according to typefunc.
228- """
229- if mode_id is None:
230- mode_id = trans_id
231- try:
232- old_path = self._tree_id_paths[mode_id]
233- except KeyError:
234- return
235- try:
236- mode = os.stat(self._tree.abspath(old_path)).st_mode
237- except OSError, e:
238- if e.errno in (errno.ENOENT, errno.ENOTDIR):
239- # Either old_path doesn't exist, or the parent of the
240- # target is not a directory (but will be one eventually)
241- # Either way, we know it doesn't exist *right now*
242- # See also bug #248448
243- return
244- else:
245- raise
246- if typefunc(mode):
247- os.chmod(self._limbo_name(trans_id), mode)
248-
249- def create_hardlink(self, path, trans_id):
250- """Schedule creation of a hard link"""
251- name = self._limbo_name(trans_id)
252- try:
253- os.link(path, name)
254- except OSError, e:
255- if e.errno != errno.EPERM:
256- raise
257- raise errors.HardLinkNotSupported(path)
258- try:
259- unique_add(self._new_contents, trans_id, 'file')
260- except:
261- # Clean up the file, it never got registered so
262- # TreeTransform.finalize() won't clean it up.
263- os.unlink(name)
264- raise
265-
266- def create_directory(self, trans_id):
267- """Schedule creation of a new directory.
268-
269- See also new_directory.
270- """
271- os.mkdir(self._limbo_name(trans_id))
272- unique_add(self._new_contents, trans_id, 'directory')
273-
274- def create_symlink(self, target, trans_id):
275- """Schedule creation of a new symbolic link.
276-
277- target is a bytestring.
278- See also new_symlink.
279- """
280- if has_symlinks():
281- os.symlink(target, self._limbo_name(trans_id))
282- unique_add(self._new_contents, trans_id, 'symlink')
283- else:
284- try:
285- path = FinalPaths(self).get_path(trans_id)
286- except KeyError:
287- path = None
288- raise UnableCreateSymlink(path=path)
289-
290- def cancel_creation(self, trans_id):
291- """Cancel the creation of new file contents."""
292- del self._new_contents[trans_id]
293- children = self._limbo_children.get(trans_id)
294- # if this is a limbo directory with children, move them before removing
295- # the directory
296- if children is not None:
297- self._rename_in_limbo(children)
298- del self._limbo_children[trans_id]
299- del self._limbo_children_names[trans_id]
300- delete_any(self._limbo_name(trans_id))
301-
302 def delete_contents(self, trans_id):
303 """Schedule the contents of a path entry for deletion"""
304 self.tree_kind(trans_id)
305@@ -518,22 +330,6 @@
306 new_ids.update(changed_kind)
307 return sorted(FinalPaths(self).get_paths(new_ids))
308
309- def tree_kind(self, trans_id):
310- """Determine the file kind in the working tree.
311-
312- Raises NoSuchFile if the file does not exist
313- """
314- path = self._tree_id_paths.get(trans_id)
315- if path is None:
316- raise NoSuchFile(None)
317- try:
318- return file_kind(self._tree.abspath(path))
319- except OSError, e:
320- if e.errno != errno.ENOENT:
321- raise
322- else:
323- raise NoSuchFile(path)
324-
325 def final_kind(self, trans_id):
326 """Determine the final file kind, after any changes applied.
327
328@@ -667,26 +463,6 @@
329 # ensure that all children are registered with the transaction
330 list(self.iter_tree_children(parent_id))
331
332- def iter_tree_children(self, parent_id):
333- """Iterate through the entry's tree children, if any"""
334- try:
335- path = self._tree_id_paths[parent_id]
336- except KeyError:
337- return
338- try:
339- children = os.listdir(self._tree.abspath(path))
340- except OSError, e:
341- if not (osutils._is_error_enotdir(e)
342- or e.errno in (errno.ENOENT, errno.ESRCH)):
343- raise
344- return
345-
346- for child in children:
347- childpath = joinpath(path, child)
348- if self._tree.is_control_filename(childpath):
349- continue
350- yield self.trans_id_tree_path(childpath)
351-
352 def has_named_child(self, by_parent, parent_id, name):
353 try:
354 children = by_parent[parent_id]
355@@ -867,50 +643,6 @@
356 return True
357 return False
358
359- def _limbo_name(self, trans_id):
360- """Generate the limbo name of a file"""
361- limbo_name = self._limbo_files.get(trans_id)
362- if limbo_name is not None:
363- return limbo_name
364- parent = self._new_parent.get(trans_id)
365- # if the parent directory is already in limbo (e.g. when building a
366- # tree), choose a limbo name inside the parent, to reduce further
367- # renames.
368- use_direct_path = False
369- if self._new_contents.get(parent) == 'directory':
370- filename = self._new_name.get(trans_id)
371- if filename is not None:
372- if parent not in self._limbo_children:
373- self._limbo_children[parent] = set()
374- self._limbo_children_names[parent] = {}
375- use_direct_path = True
376- # the direct path can only be used if no other file has
377- # already taken this pathname, i.e. if the name is unused, or
378- # if it is already associated with this trans_id.
379- elif self._case_sensitive_target:
380- if (self._limbo_children_names[parent].get(filename)
381- in (trans_id, None)):
382- use_direct_path = True
383- else:
384- for l_filename, l_trans_id in\
385- self._limbo_children_names[parent].iteritems():
386- if l_trans_id == trans_id:
387- continue
388- if l_filename.lower() == filename.lower():
389- break
390- else:
391- use_direct_path = True
392-
393- if use_direct_path:
394- limbo_name = pathjoin(self._limbo_files[parent], filename)
395- self._limbo_children[parent].add(trans_id)
396- self._limbo_children_names[parent][filename] = trans_id
397- else:
398- limbo_name = pathjoin(self._limbodir, trans_id)
399- self._needs_rename.add(trans_id)
400- self._limbo_files[trans_id] = limbo_name
401- return limbo_name
402-
403 def _set_executability(self, path, trans_id):
404 """Set the executability of versioned files """
405 if supports_executable():
406@@ -1176,21 +908,17 @@
407 (('attribs',),))
408 for trans_id, kind in self._new_contents.items():
409 if kind == 'file':
410- cur_file = open(self._limbo_name(trans_id), 'rb')
411- try:
412- lines = osutils.chunks_to_lines(cur_file.readlines())
413- finally:
414- cur_file.close()
415+ lines = osutils.chunks_to_lines(
416+ self._read_file_chunks(trans_id))
417 parents = self._get_parents_lines(trans_id)
418 mpdiff = multiparent.MultiParent.from_lines(lines, parents)
419 content = ''.join(mpdiff.to_patch())
420 if kind == 'directory':
421 content = ''
422 if kind == 'symlink':
423- content = os.readlink(self._limbo_name(trans_id))
424+ content = self._read_symlink_target(trans_id)
425 yield serializer.bytes_record(content, ((trans_id, kind),))
426
427-
428 def deserialize(self, records):
429 """Deserialize a stored TreeTransform.
430
431@@ -1227,7 +955,228 @@
432 self.create_symlink(content.decode('utf-8'), trans_id)
433
434
435-class TreeTransform(TreeTransformBase):
436+class DiskTreeTransform(TreeTransformBase):
437+ """Tree transform storing its contents on disk."""
438+
439+ def __init__(self, tree, limbodir, pb=DummyProgress(),
440+ case_sensitive=True):
441+ """Constructor.
442+ :param tree: The tree that will be transformed, but not necessarily
443+ the output tree.
444+ :param limbodir: A directory where new files can be stored until
445+ they are installed in their proper places
446+ :param pb: A ProgressBar indicating how much progress is being made
447+ :param case_sensitive: If True, the target of the transform is
448+ case sensitive, not just case preserving.
449+ """
450+ TreeTransformBase.__init__(self, tree, pb, case_sensitive)
451+ self._limbodir = limbodir
452+ self._deletiondir = None
453+ # A mapping of transform ids to their limbo filename
454+ self._limbo_files = {}
455+ # A mapping of transform ids to a set of the transform ids of children
456+ # that their limbo directory has
457+ self._limbo_children = {}
458+ # Map transform ids to maps of child filename to child transform id
459+ self._limbo_children_names = {}
460+ # List of transform ids that need to be renamed from limbo into place
461+ self._needs_rename = set()
462+
463+ def finalize(self):
464+ """Release the working tree lock, if held, clean up limbo dir.
465+
466+ This is required if apply has not been invoked, but can be invoked
467+ even after apply.
468+ """
469+ if self._tree is None:
470+ return
471+ try:
472+ entries = [(self._limbo_name(t), t, k) for t, k in
473+ self._new_contents.iteritems()]
474+ entries.sort(reverse=True)
475+ for path, trans_id, kind in entries:
476+ if kind == "directory":
477+ os.rmdir(path)
478+ else:
479+ os.unlink(path)
480+ try:
481+ os.rmdir(self._limbodir)
482+ except OSError:
483+ # We don't especially care *why* the dir is immortal.
484+ raise ImmortalLimbo(self._limbodir)
485+ try:
486+ if self._deletiondir is not None:
487+ os.rmdir(self._deletiondir)
488+ except OSError:
489+ raise errors.ImmortalPendingDeletion(self._deletiondir)
490+ finally:
491+ TreeTransformBase.finalize(self)
492+
493+ def _limbo_name(self, trans_id):
494+ """Generate the limbo name of a file"""
495+ limbo_name = self._limbo_files.get(trans_id)
496+ if limbo_name is not None:
497+ return limbo_name
498+ parent = self._new_parent.get(trans_id)
499+ # if the parent directory is already in limbo (e.g. when building a
500+ # tree), choose a limbo name inside the parent, to reduce further
501+ # renames.
502+ use_direct_path = False
503+ if self._new_contents.get(parent) == 'directory':
504+ filename = self._new_name.get(trans_id)
505+ if filename is not None:
506+ if parent not in self._limbo_children:
507+ self._limbo_children[parent] = set()
508+ self._limbo_children_names[parent] = {}
509+ use_direct_path = True
510+ # the direct path can only be used if no other file has
511+ # already taken this pathname, i.e. if the name is unused, or
512+ # if it is already associated with this trans_id.
513+ elif self._case_sensitive_target:
514+ if (self._limbo_children_names[parent].get(filename)
515+ in (trans_id, None)):
516+ use_direct_path = True
517+ else:
518+ for l_filename, l_trans_id in\
519+ self._limbo_children_names[parent].iteritems():
520+ if l_trans_id == trans_id:
521+ continue
522+ if l_filename.lower() == filename.lower():
523+ break
524+ else:
525+ use_direct_path = True
526+
527+ if use_direct_path:
528+ limbo_name = pathjoin(self._limbo_files[parent], filename)
529+ self._limbo_children[parent].add(trans_id)
530+ self._limbo_children_names[parent][filename] = trans_id
531+ else:
532+ limbo_name = pathjoin(self._limbodir, trans_id)
533+ self._needs_rename.add(trans_id)
534+ self._limbo_files[trans_id] = limbo_name
535+ return limbo_name
536+
537+ def adjust_path(self, name, parent, trans_id):
538+ previous_parent = self._new_parent.get(trans_id)
539+ previous_name = self._new_name.get(trans_id)
540+ TreeTransformBase.adjust_path(self, name, parent, trans_id)
541+ if (trans_id in self._limbo_files and
542+ trans_id not in self._needs_rename):
543+ self._rename_in_limbo([trans_id])
544+ self._limbo_children[previous_parent].remove(trans_id)
545+ del self._limbo_children_names[previous_parent][previous_name]
546+
547+ def _rename_in_limbo(self, trans_ids):
548+ """Fix limbo names so that the right final path is produced.
549+
550+ This means we outsmarted ourselves-- we tried to avoid renaming
551+ these files later by creating them with their final names in their
552+ final parents. But now the previous name or parent is no longer
553+ suitable, so we have to rename them.
554+
555+ Even for trans_ids that have no new contents, we must remove their
556+ entries from _limbo_files, because they are now stale.
557+ """
558+ for trans_id in trans_ids:
559+ old_path = self._limbo_files.pop(trans_id)
560+ if trans_id not in self._new_contents:
561+ continue
562+ new_path = self._limbo_name(trans_id)
563+ os.rename(old_path, new_path)
564+
565+ def create_file(self, contents, trans_id, mode_id=None):
566+ """Schedule creation of a new file.
567+
568+ See also new_file.
569+
570+ Contents is an iterator of strings, all of which will be written
571+ to the target destination.
572+
573+ New file takes the permissions of any existing file with that id,
574+ unless mode_id is specified.
575+ """
576+ name = self._limbo_name(trans_id)
577+ f = open(name, 'wb')
578+ try:
579+ try:
580+ unique_add(self._new_contents, trans_id, 'file')
581+ except:
582+ # Clean up the file, it never got registered so
583+ # TreeTransform.finalize() won't clean it up.
584+ f.close()
585+ os.unlink(name)
586+ raise
587+
588+ f.writelines(contents)
589+ finally:
590+ f.close()
591+ self._set_mode(trans_id, mode_id, S_ISREG)
592+
593+ def _read_file_chunks(self, trans_id):
594+ cur_file = open(self._limbo_name(trans_id), 'rb')
595+ try:
596+ return cur_file.readlines()
597+ finally:
598+ cur_file.close()
599+
600+ def _read_symlink_target(self, trans_id):
601+ return os.readlink(self._limbo_name(trans_id))
602+
603+ def create_hardlink(self, path, trans_id):
604+ """Schedule creation of a hard link"""
605+ name = self._limbo_name(trans_id)
606+ try:
607+ os.link(path, name)
608+ except OSError, e:
609+ if e.errno != errno.EPERM:
610+ raise
611+ raise errors.HardLinkNotSupported(path)
612+ try:
613+ unique_add(self._new_contents, trans_id, 'file')
614+ except:
615+ # Clean up the file, it never got registered so
616+ # TreeTransform.finalize() won't clean it up.
617+ os.unlink(name)
618+ raise
619+
620+ def create_directory(self, trans_id):
621+ """Schedule creation of a new directory.
622+
623+ See also new_directory.
624+ """
625+ os.mkdir(self._limbo_name(trans_id))
626+ unique_add(self._new_contents, trans_id, 'directory')
627+
628+ def create_symlink(self, target, trans_id):
629+ """Schedule creation of a new symbolic link.
630+
631+ target is a bytestring.
632+ See also new_symlink.
633+ """
634+ if has_symlinks():
635+ os.symlink(target, self._limbo_name(trans_id))
636+ unique_add(self._new_contents, trans_id, 'symlink')
637+ else:
638+ try:
639+ path = FinalPaths(self).get_path(trans_id)
640+ except KeyError:
641+ path = None
642+ raise UnableCreateSymlink(path=path)
643+
644+ def cancel_creation(self, trans_id):
645+ """Cancel the creation of new file contents."""
646+ del self._new_contents[trans_id]
647+ children = self._limbo_children.get(trans_id)
648+ # if this is a limbo directory with children, move them before removing
649+ # the directory
650+ if children is not None:
651+ self._rename_in_limbo(children)
652+ del self._limbo_children[trans_id]
653+ del self._limbo_children_names[trans_id]
654+ delete_any(self._limbo_name(trans_id))
655+
656+
657+class TreeTransform(DiskTreeTransform):
658 """Represent a tree transformation.
659
660 This object is designed to support incremental generation of the transform,
661@@ -1319,10 +1268,96 @@
662 tree.unlock()
663 raise
664
665- TreeTransformBase.__init__(self, tree, limbodir, pb,
666+ # Cache of realpath results, to speed up canonical_path
667+ self._realpaths = {}
668+ # Cache of relpath results, to speed up canonical_path
669+ self._relpaths = {}
670+ DiskTreeTransform.__init__(self, tree, limbodir, pb,
671 tree.case_sensitive)
672 self._deletiondir = deletiondir
673
674+ def canonical_path(self, path):
675+ """Get the canonical tree-relative path"""
676+ # don't follow final symlinks
677+ abs = self._tree.abspath(path)
678+ if abs in self._relpaths:
679+ return self._relpaths[abs]
680+ dirname, basename = os.path.split(abs)
681+ if dirname not in self._realpaths:
682+ self._realpaths[dirname] = os.path.realpath(dirname)
683+ dirname = self._realpaths[dirname]
684+ abs = pathjoin(dirname, basename)
685+ if dirname in self._relpaths:
686+ relpath = pathjoin(self._relpaths[dirname], basename)
687+ relpath = relpath.rstrip('/\\')
688+ else:
689+ relpath = self._tree.relpath(abs)
690+ self._relpaths[abs] = relpath
691+ return relpath
692+
693+ def tree_kind(self, trans_id):
694+ """Determine the file kind in the working tree.
695+
696+ Raises NoSuchFile if the file does not exist
697+ """
698+ path = self._tree_id_paths.get(trans_id)
699+ if path is None:
700+ raise NoSuchFile(None)
701+ try:
702+ return file_kind(self._tree.abspath(path))
703+ except OSError, e:
704+ if e.errno != errno.ENOENT:
705+ raise
706+ else:
707+ raise NoSuchFile(path)
708+
709+ def _set_mode(self, trans_id, mode_id, typefunc):
710+ """Set the mode of new file contents.
711+ The mode_id is the existing file to get the mode from (often the same
712+ as trans_id). The operation is only performed if there's a mode match
713+ according to typefunc.
714+ """
715+ if mode_id is None:
716+ mode_id = trans_id
717+ try:
718+ old_path = self._tree_id_paths[mode_id]
719+ except KeyError:
720+ return
721+ try:
722+ mode = os.stat(self._tree.abspath(old_path)).st_mode
723+ except OSError, e:
724+ if e.errno in (errno.ENOENT, errno.ENOTDIR):
725+ # Either old_path doesn't exist, or the parent of the
726+ # target is not a directory (but will be one eventually)
727+ # Either way, we know it doesn't exist *right now*
728+ # See also bug #248448
729+ return
730+ else:
731+ raise
732+ if typefunc(mode):
733+ os.chmod(self._limbo_name(trans_id), mode)
734+
735+ def iter_tree_children(self, parent_id):
736+ """Iterate through the entry's tree children, if any"""
737+ try:
738+ path = self._tree_id_paths[parent_id]
739+ except KeyError:
740+ return
741+ try:
742+ children = os.listdir(self._tree.abspath(path))
743+ except OSError, e:
744+ if not (osutils._is_error_enotdir(e)
745+ or e.errno in (errno.ENOENT, errno.ESRCH)):
746+ raise
747+ return
748+
749+ for child in children:
750+ childpath = joinpath(path, child)
751+ if self._tree.is_control_filename(childpath):
752+ continue
753+ yield self.trans_id_tree_path(childpath)
754+
755+
756 def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
757 """Apply all changes to the inventory and filesystem.
758
759@@ -1505,7 +1540,7 @@
760 return modified_paths
761
762
763-class TransformPreview(TreeTransformBase):
764+class TransformPreview(DiskTreeTransform):
765 """A TreeTransform for generating preview trees.
766
767 Unlike TreeTransform, this version works when the input tree is a
768@@ -1516,7 +1551,7 @@
769 def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
770 tree.lock_read()
771 limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
772- TreeTransformBase.__init__(self, tree, limbodir, pb, case_sensitive)
773+ DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
774
775 def canonical_path(self, path):
776 return path