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

Proposed by Aaron Bentley
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 Approve
Review via email: mp+6635@code.launchpad.net
To post a comment you must log in.
Revision history for this message
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-----

Revision history for this message
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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2009-05-23 04:55:52 +0000
+++ NEWS 2009-05-28 08:35:37 +0000
@@ -34,6 +34,10 @@
3434
35* Added osutils.parent_directories(). (Ian Clatworthy)35* Added osutils.parent_directories(). (Ian Clatworthy)
3636
37* TreeTransformBase no longer assumes that limbo is provided via disk.
38 DiskTreeTransform now provides disk functionality. (Aaron Bentley)
39
40
37Internals41Internals
38*********42*********
3943
4044
=== modified file 'bzrlib/transform.py'
--- bzrlib/transform.py 2009-05-23 04:55:52 +0000
+++ bzrlib/transform.py 2009-05-28 08:35:37 +0000
@@ -76,24 +76,20 @@
7676
7777
78class TreeTransformBase(object):78class TreeTransformBase(object):
79 """The base class for TreeTransform and TreeTransformBase"""79 """The base class for TreeTransform and its kin."""
8080
81 def __init__(self, tree, limbodir, pb=DummyProgress(),81 def __init__(self, tree, pb=DummyProgress(),
82 case_sensitive=True):82 case_sensitive=True):
83 """Constructor.83 """Constructor.
8484
85 :param tree: The tree that will be transformed, but not necessarily85 :param tree: The tree that will be transformed, but not necessarily
86 the output tree.86 the output tree.
87 :param limbodir: A directory where new files can be stored until
88 they are installed in their proper places
89 :param pb: A ProgressBar indicating how much progress is being made87 :param pb: A ProgressBar indicating how much progress is being made
90 :param case_sensitive: If True, the target of the transform is88 :param case_sensitive: If True, the target of the transform is
91 case sensitive, not just case preserving.89 case sensitive, not just case preserving.
92 """90 """
93 object.__init__(self)91 object.__init__(self)
94 self._tree = tree92 self._tree = tree
95 self._limbodir = limbodir
96 self._deletiondir = None
97 self._id_number = 093 self._id_number = 0
98 # mapping of trans_id -> new basename94 # mapping of trans_id -> new basename
99 self._new_name = {}95 self._new_name = {}
@@ -101,15 +97,6 @@
101 self._new_parent = {}97 self._new_parent = {}
102 # mapping of trans_id with new contents -> new file_kind98 # mapping of trans_id with new contents -> new file_kind
103 self._new_contents = {}99 self._new_contents = {}
104 # A mapping of transform ids to their limbo filename
105 self._limbo_files = {}
106 # A mapping of transform ids to a set of the transform ids of children
107 # that their limbo directory has
108 self._limbo_children = {}
109 # Map transform ids to maps of child filename to child transform id
110 self._limbo_children_names = {}
111 # List of transform ids that need to be renamed from limbo into place
112 self._needs_rename = set()
113 # Set of trans_ids whose contents will be removed100 # Set of trans_ids whose contents will be removed
114 self._removed_contents = set()101 self._removed_contents = set()
115 # Mapping of trans_id -> new execute-bit value102 # Mapping of trans_id -> new execute-bit value
@@ -128,10 +115,6 @@
128 self._tree_path_ids = {}115 self._tree_path_ids = {}
129 # Mapping trans_id -> path in old tree116 # Mapping trans_id -> path in old tree
130 self._tree_id_paths = {}117 self._tree_id_paths = {}
131 # Cache of realpath results, to speed up canonical_path
132 self._realpaths = {}
133 # Cache of relpath results, to speed up canonical_path
134 self._relpaths = {}
135 # The trans_id that will be used as the tree root118 # The trans_id that will be used as the tree root
136 root_id = tree.get_root_id()119 root_id = tree.get_root_id()
137 if root_id is not None:120 if root_id is not None:
@@ -147,42 +130,22 @@
147 # A counter of how many files have been renamed130 # A counter of how many files have been renamed
148 self.rename_count = 0131 self.rename_count = 0
149132
133 def finalize(self):
134 """Release the working tree lock, if held.
135
136 This is required if apply has not been invoked, but can be invoked
137 even after apply.
138 """
139 if self._tree is None:
140 return
141 self._tree.unlock()
142 self._tree = None
143
150 def __get_root(self):144 def __get_root(self):
151 return self._new_root145 return self._new_root
152146
153 root = property(__get_root)147 root = property(__get_root)
154148
155 def finalize(self):
156 """Release the working tree lock, if held, clean up limbo dir.
157
158 This is required if apply has not been invoked, but can be invoked
159 even after apply.
160 """
161 if self._tree is None:
162 return
163 try:
164 entries = [(self._limbo_name(t), t, k) for t, k in
165 self._new_contents.iteritems()]
166 entries.sort(reverse=True)
167 for path, trans_id, kind in entries:
168 if kind == "directory":
169 os.rmdir(path)
170 else:
171 os.unlink(path)
172 try:
173 os.rmdir(self._limbodir)
174 except OSError:
175 # We don't especially care *why* the dir is immortal.
176 raise ImmortalLimbo(self._limbodir)
177 try:
178 if self._deletiondir is not None:
179 os.rmdir(self._deletiondir)
180 except OSError:
181 raise errors.ImmortalPendingDeletion(self._deletiondir)
182 finally:
183 self._tree.unlock()
184 self._tree = None
185
186 def _assign_id(self):149 def _assign_id(self):
187 """Produce a new tranform id"""150 """Produce a new tranform id"""
188 new_id = "new-%s" % self._id_number151 new_id = "new-%s" % self._id_number
@@ -200,37 +163,12 @@
200 """Change the path that is assigned to a transaction id."""163 """Change the path that is assigned to a transaction id."""
201 if trans_id == self._new_root:164 if trans_id == self._new_root:
202 raise CantMoveRoot165 raise CantMoveRoot
203 previous_parent = self._new_parent.get(trans_id)
204 previous_name = self._new_name.get(trans_id)
205 self._new_name[trans_id] = name166 self._new_name[trans_id] = name
206 self._new_parent[trans_id] = parent167 self._new_parent[trans_id] = parent
207 if parent == ROOT_PARENT:168 if parent == ROOT_PARENT:
208 if self._new_root is not None:169 if self._new_root is not None:
209 raise ValueError("Cannot have multiple roots.")170 raise ValueError("Cannot have multiple roots.")
210 self._new_root = trans_id171 self._new_root = trans_id
211 if (trans_id in self._limbo_files and
212 trans_id not in self._needs_rename):
213 self._rename_in_limbo([trans_id])
214 self._limbo_children[previous_parent].remove(trans_id)
215 del self._limbo_children_names[previous_parent][previous_name]
216
217 def _rename_in_limbo(self, trans_ids):
218 """Fix limbo names so that the right final path is produced.
219
220 This means we outsmarted ourselves-- we tried to avoid renaming
221 these files later by creating them with their final names in their
222 final parents. But now the previous name or parent is no longer
223 suitable, so we have to rename them.
224
225 Even for trans_ids that have no new contents, we must remove their
226 entries from _limbo_files, because they are now stale.
227 """
228 for trans_id in trans_ids:
229 old_path = self._limbo_files.pop(trans_id)
230 if trans_id not in self._new_contents:
231 continue
232 new_path = self._limbo_name(trans_id)
233 os.rename(old_path, new_path)
234172
235 def adjust_root_path(self, name, parent):173 def adjust_root_path(self, name, parent):
236 """Emulate moving the root by moving all children, instead.174 """Emulate moving the root by moving all children, instead.
@@ -298,25 +236,6 @@
298 else:236 else:
299 return self.trans_id_tree_file_id(file_id)237 return self.trans_id_tree_file_id(file_id)
300238
301 def canonical_path(self, path):
302 """Get the canonical tree-relative path"""
303 # don't follow final symlinks
304 abs = self._tree.abspath(path)
305 if abs in self._relpaths:
306 return self._relpaths[abs]
307 dirname, basename = os.path.split(abs)
308 if dirname not in self._realpaths:
309 self._realpaths[dirname] = os.path.realpath(dirname)
310 dirname = self._realpaths[dirname]
311 abs = pathjoin(dirname, basename)
312 if dirname in self._relpaths:
313 relpath = pathjoin(self._relpaths[dirname], basename)
314 relpath = relpath.rstrip('/\\')
315 else:
316 relpath = self._tree.relpath(abs)
317 self._relpaths[abs] = relpath
318 return relpath
319
320 def trans_id_tree_path(self, path):239 def trans_id_tree_path(self, path):
321 """Determine (and maybe set) the transaction ID for a tree path."""240 """Determine (and maybe set) the transaction ID for a tree path."""
322 path = self.canonical_path(path)241 path = self.canonical_path(path)
@@ -332,113 +251,6 @@
332 return ROOT_PARENT251 return ROOT_PARENT
333 return self.trans_id_tree_path(os.path.dirname(path))252 return self.trans_id_tree_path(os.path.dirname(path))
334253
335 def create_file(self, contents, trans_id, mode_id=None):
336 """Schedule creation of a new file.
337
338 See also new_file.
339
340 Contents is an iterator of strings, all of which will be written
341 to the target destination.
342
343 New file takes the permissions of any existing file with that id,
344 unless mode_id is specified.
345 """
346 name = self._limbo_name(trans_id)
347 f = open(name, 'wb')
348 try:
349 try:
350 unique_add(self._new_contents, trans_id, 'file')
351 except:
352 # Clean up the file, it never got registered so
353 # TreeTransform.finalize() won't clean it up.
354 f.close()
355 os.unlink(name)
356 raise
357
358 f.writelines(contents)
359 finally:
360 f.close()
361 self._set_mode(trans_id, mode_id, S_ISREG)
362
363 def _set_mode(self, trans_id, mode_id, typefunc):
364 """Set the mode of new file contents.
365 The mode_id is the existing file to get the mode from (often the same
366 as trans_id). The operation is only performed if there's a mode match
367 according to typefunc.
368 """
369 if mode_id is None:
370 mode_id = trans_id
371 try:
372 old_path = self._tree_id_paths[mode_id]
373 except KeyError:
374 return
375 try:
376 mode = os.stat(self._tree.abspath(old_path)).st_mode
377 except OSError, e:
378 if e.errno in (errno.ENOENT, errno.ENOTDIR):
379 # Either old_path doesn't exist, or the parent of the
380 # target is not a directory (but will be one eventually)
381 # Either way, we know it doesn't exist *right now*
382 # See also bug #248448
383 return
384 else:
385 raise
386 if typefunc(mode):
387 os.chmod(self._limbo_name(trans_id), mode)
388
389 def create_hardlink(self, path, trans_id):
390 """Schedule creation of a hard link"""
391 name = self._limbo_name(trans_id)
392 try:
393 os.link(path, name)
394 except OSError, e:
395 if e.errno != errno.EPERM:
396 raise
397 raise errors.HardLinkNotSupported(path)
398 try:
399 unique_add(self._new_contents, trans_id, 'file')
400 except:
401 # Clean up the file, it never got registered so
402 # TreeTransform.finalize() won't clean it up.
403 os.unlink(name)
404 raise
405
406 def create_directory(self, trans_id):
407 """Schedule creation of a new directory.
408
409 See also new_directory.
410 """
411 os.mkdir(self._limbo_name(trans_id))
412 unique_add(self._new_contents, trans_id, 'directory')
413
414 def create_symlink(self, target, trans_id):
415 """Schedule creation of a new symbolic link.
416
417 target is a bytestring.
418 See also new_symlink.
419 """
420 if has_symlinks():
421 os.symlink(target, self._limbo_name(trans_id))
422 unique_add(self._new_contents, trans_id, 'symlink')
423 else:
424 try:
425 path = FinalPaths(self).get_path(trans_id)
426 except KeyError:
427 path = None
428 raise UnableCreateSymlink(path=path)
429
430 def cancel_creation(self, trans_id):
431 """Cancel the creation of new file contents."""
432 del self._new_contents[trans_id]
433 children = self._limbo_children.get(trans_id)
434 # if this is a limbo directory with children, move them before removing
435 # the directory
436 if children is not None:
437 self._rename_in_limbo(children)
438 del self._limbo_children[trans_id]
439 del self._limbo_children_names[trans_id]
440 delete_any(self._limbo_name(trans_id))
441
442 def delete_contents(self, trans_id):254 def delete_contents(self, trans_id):
443 """Schedule the contents of a path entry for deletion"""255 """Schedule the contents of a path entry for deletion"""
444 self.tree_kind(trans_id)256 self.tree_kind(trans_id)
@@ -518,22 +330,6 @@
518 new_ids.update(changed_kind)330 new_ids.update(changed_kind)
519 return sorted(FinalPaths(self).get_paths(new_ids))331 return sorted(FinalPaths(self).get_paths(new_ids))
520332
521 def tree_kind(self, trans_id):
522 """Determine the file kind in the working tree.
523
524 Raises NoSuchFile if the file does not exist
525 """
526 path = self._tree_id_paths.get(trans_id)
527 if path is None:
528 raise NoSuchFile(None)
529 try:
530 return file_kind(self._tree.abspath(path))
531 except OSError, e:
532 if e.errno != errno.ENOENT:
533 raise
534 else:
535 raise NoSuchFile(path)
536
537 def final_kind(self, trans_id):333 def final_kind(self, trans_id):
538 """Determine the final file kind, after any changes applied.334 """Determine the final file kind, after any changes applied.
539335
@@ -667,26 +463,6 @@
667 # ensure that all children are registered with the transaction463 # ensure that all children are registered with the transaction
668 list(self.iter_tree_children(parent_id))464 list(self.iter_tree_children(parent_id))
669465
670 def iter_tree_children(self, parent_id):
671 """Iterate through the entry's tree children, if any"""
672 try:
673 path = self._tree_id_paths[parent_id]
674 except KeyError:
675 return
676 try:
677 children = os.listdir(self._tree.abspath(path))
678 except OSError, e:
679 if not (osutils._is_error_enotdir(e)
680 or e.errno in (errno.ENOENT, errno.ESRCH)):
681 raise
682 return
683
684 for child in children:
685 childpath = joinpath(path, child)
686 if self._tree.is_control_filename(childpath):
687 continue
688 yield self.trans_id_tree_path(childpath)
689
690 def has_named_child(self, by_parent, parent_id, name):466 def has_named_child(self, by_parent, parent_id, name):
691 try:467 try:
692 children = by_parent[parent_id]468 children = by_parent[parent_id]
@@ -867,50 +643,6 @@
867 return True643 return True
868 return False644 return False
869645
870 def _limbo_name(self, trans_id):
871 """Generate the limbo name of a file"""
872 limbo_name = self._limbo_files.get(trans_id)
873 if limbo_name is not None:
874 return limbo_name
875 parent = self._new_parent.get(trans_id)
876 # if the parent directory is already in limbo (e.g. when building a
877 # tree), choose a limbo name inside the parent, to reduce further
878 # renames.
879 use_direct_path = False
880 if self._new_contents.get(parent) == 'directory':
881 filename = self._new_name.get(trans_id)
882 if filename is not None:
883 if parent not in self._limbo_children:
884 self._limbo_children[parent] = set()
885 self._limbo_children_names[parent] = {}
886 use_direct_path = True
887 # the direct path can only be used if no other file has
888 # already taken this pathname, i.e. if the name is unused, or
889 # if it is already associated with this trans_id.
890 elif self._case_sensitive_target:
891 if (self._limbo_children_names[parent].get(filename)
892 in (trans_id, None)):
893 use_direct_path = True
894 else:
895 for l_filename, l_trans_id in\
896 self._limbo_children_names[parent].iteritems():
897 if l_trans_id == trans_id:
898 continue
899 if l_filename.lower() == filename.lower():
900 break
901 else:
902 use_direct_path = True
903
904 if use_direct_path:
905 limbo_name = pathjoin(self._limbo_files[parent], filename)
906 self._limbo_children[parent].add(trans_id)
907 self._limbo_children_names[parent][filename] = trans_id
908 else:
909 limbo_name = pathjoin(self._limbodir, trans_id)
910 self._needs_rename.add(trans_id)
911 self._limbo_files[trans_id] = limbo_name
912 return limbo_name
913
914 def _set_executability(self, path, trans_id):646 def _set_executability(self, path, trans_id):
915 """Set the executability of versioned files """647 """Set the executability of versioned files """
916 if supports_executable():648 if supports_executable():
@@ -1176,21 +908,17 @@
1176 (('attribs',),))908 (('attribs',),))
1177 for trans_id, kind in self._new_contents.items():909 for trans_id, kind in self._new_contents.items():
1178 if kind == 'file':910 if kind == 'file':
1179 cur_file = open(self._limbo_name(trans_id), 'rb')911 lines = osutils.chunks_to_lines(
1180 try:912 self._read_file_chunks(trans_id))
1181 lines = osutils.chunks_to_lines(cur_file.readlines())
1182 finally:
1183 cur_file.close()
1184 parents = self._get_parents_lines(trans_id)913 parents = self._get_parents_lines(trans_id)
1185 mpdiff = multiparent.MultiParent.from_lines(lines, parents)914 mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1186 content = ''.join(mpdiff.to_patch())915 content = ''.join(mpdiff.to_patch())
1187 if kind == 'directory':916 if kind == 'directory':
1188 content = ''917 content = ''
1189 if kind == 'symlink':918 if kind == 'symlink':
1190 content = os.readlink(self._limbo_name(trans_id))919 content = self._read_symlink_target(trans_id)
1191 yield serializer.bytes_record(content, ((trans_id, kind),))920 yield serializer.bytes_record(content, ((trans_id, kind),))
1192921
1193
1194 def deserialize(self, records):922 def deserialize(self, records):
1195 """Deserialize a stored TreeTransform.923 """Deserialize a stored TreeTransform.
1196924
@@ -1227,7 +955,228 @@
1227 self.create_symlink(content.decode('utf-8'), trans_id)955 self.create_symlink(content.decode('utf-8'), trans_id)
1228956
1229957
1230class TreeTransform(TreeTransformBase):958class DiskTreeTransform(TreeTransformBase):
959 """Tree transform storing its contents on disk."""
960
961 def __init__(self, tree, limbodir, pb=DummyProgress(),
962 case_sensitive=True):
963 """Constructor.
964 :param tree: The tree that will be transformed, but not necessarily
965 the output tree.
966 :param limbodir: A directory where new files can be stored until
967 they are installed in their proper places
968 :param pb: A ProgressBar indicating how much progress is being made
969 :param case_sensitive: If True, the target of the transform is
970 case sensitive, not just case preserving.
971 """
972 TreeTransformBase.__init__(self, tree, pb, case_sensitive)
973 self._limbodir = limbodir
974 self._deletiondir = None
975 # A mapping of transform ids to their limbo filename
976 self._limbo_files = {}
977 # A mapping of transform ids to a set of the transform ids of children
978 # that their limbo directory has
979 self._limbo_children = {}
980 # Map transform ids to maps of child filename to child transform id
981 self._limbo_children_names = {}
982 # List of transform ids that need to be renamed from limbo into place
983 self._needs_rename = set()
984
985 def finalize(self):
986 """Release the working tree lock, if held, clean up limbo dir.
987
988 This is required if apply has not been invoked, but can be invoked
989 even after apply.
990 """
991 if self._tree is None:
992 return
993 try:
994 entries = [(self._limbo_name(t), t, k) for t, k in
995 self._new_contents.iteritems()]
996 entries.sort(reverse=True)
997 for path, trans_id, kind in entries:
998 if kind == "directory":
999 os.rmdir(path)
1000 else:
1001 os.unlink(path)
1002 try:
1003 os.rmdir(self._limbodir)
1004 except OSError:
1005 # We don't especially care *why* the dir is immortal.
1006 raise ImmortalLimbo(self._limbodir)
1007 try:
1008 if self._deletiondir is not None:
1009 os.rmdir(self._deletiondir)
1010 except OSError:
1011 raise errors.ImmortalPendingDeletion(self._deletiondir)
1012 finally:
1013 TreeTransformBase.finalize(self)
1014
1015 def _limbo_name(self, trans_id):
1016 """Generate the limbo name of a file"""
1017 limbo_name = self._limbo_files.get(trans_id)
1018 if limbo_name is not None:
1019 return limbo_name
1020 parent = self._new_parent.get(trans_id)
1021 # if the parent directory is already in limbo (e.g. when building a
1022 # tree), choose a limbo name inside the parent, to reduce further
1023 # renames.
1024 use_direct_path = False
1025 if self._new_contents.get(parent) == 'directory':
1026 filename = self._new_name.get(trans_id)
1027 if filename is not None:
1028 if parent not in self._limbo_children:
1029 self._limbo_children[parent] = set()
1030 self._limbo_children_names[parent] = {}
1031 use_direct_path = True
1032 # the direct path can only be used if no other file has
1033 # already taken this pathname, i.e. if the name is unused, or
1034 # if it is already associated with this trans_id.
1035 elif self._case_sensitive_target:
1036 if (self._limbo_children_names[parent].get(filename)
1037 in (trans_id, None)):
1038 use_direct_path = True
1039 else:
1040 for l_filename, l_trans_id in\
1041 self._limbo_children_names[parent].iteritems():
1042 if l_trans_id == trans_id:
1043 continue
1044 if l_filename.lower() == filename.lower():
1045 break
1046 else:
1047 use_direct_path = True
1048
1049 if use_direct_path:
1050 limbo_name = pathjoin(self._limbo_files[parent], filename)
1051 self._limbo_children[parent].add(trans_id)
1052 self._limbo_children_names[parent][filename] = trans_id
1053 else:
1054 limbo_name = pathjoin(self._limbodir, trans_id)
1055 self._needs_rename.add(trans_id)
1056 self._limbo_files[trans_id] = limbo_name
1057 return limbo_name
1058
1059 def adjust_path(self, name, parent, trans_id):
1060 previous_parent = self._new_parent.get(trans_id)
1061 previous_name = self._new_name.get(trans_id)
1062 TreeTransformBase.adjust_path(self, name, parent, trans_id)
1063 if (trans_id in self._limbo_files and
1064 trans_id not in self._needs_rename):
1065 self._rename_in_limbo([trans_id])
1066 self._limbo_children[previous_parent].remove(trans_id)
1067 del self._limbo_children_names[previous_parent][previous_name]
1068
1069 def _rename_in_limbo(self, trans_ids):
1070 """Fix limbo names so that the right final path is produced.
1071
1072 This means we outsmarted ourselves-- we tried to avoid renaming
1073 these files later by creating them with their final names in their
1074 final parents. But now the previous name or parent is no longer
1075 suitable, so we have to rename them.
1076
1077 Even for trans_ids that have no new contents, we must remove their
1078 entries from _limbo_files, because they are now stale.
1079 """
1080 for trans_id in trans_ids:
1081 old_path = self._limbo_files.pop(trans_id)
1082 if trans_id not in self._new_contents:
1083 continue
1084 new_path = self._limbo_name(trans_id)
1085 os.rename(old_path, new_path)
1086
1087 def create_file(self, contents, trans_id, mode_id=None):
1088 """Schedule creation of a new file.
1089
1090 See also new_file.
1091
1092 Contents is an iterator of strings, all of which will be written
1093 to the target destination.
1094
1095 New file takes the permissions of any existing file with that id,
1096 unless mode_id is specified.
1097 """
1098 name = self._limbo_name(trans_id)
1099 f = open(name, 'wb')
1100 try:
1101 try:
1102 unique_add(self._new_contents, trans_id, 'file')
1103 except:
1104 # Clean up the file, it never got registered so
1105 # TreeTransform.finalize() won't clean it up.
1106 f.close()
1107 os.unlink(name)
1108 raise
1109
1110 f.writelines(contents)
1111 finally:
1112 f.close()
1113 self._set_mode(trans_id, mode_id, S_ISREG)
1114
1115 def _read_file_chunks(self, trans_id):
1116 cur_file = open(self._limbo_name(trans_id), 'rb')
1117 try:
1118 return cur_file.readlines()
1119 finally:
1120 cur_file.close()
1121
1122 def _read_symlink_target(self, trans_id):
1123 return os.readlink(self._limbo_name(trans_id))
1124
1125 def create_hardlink(self, path, trans_id):
1126 """Schedule creation of a hard link"""
1127 name = self._limbo_name(trans_id)
1128 try:
1129 os.link(path, name)
1130 except OSError, e:
1131 if e.errno != errno.EPERM:
1132 raise
1133 raise errors.HardLinkNotSupported(path)
1134 try:
1135 unique_add(self._new_contents, trans_id, 'file')
1136 except:
1137 # Clean up the file, it never got registered so
1138 # TreeTransform.finalize() won't clean it up.
1139 os.unlink(name)
1140 raise
1141
1142 def create_directory(self, trans_id):
1143 """Schedule creation of a new directory.
1144
1145 See also new_directory.
1146 """
1147 os.mkdir(self._limbo_name(trans_id))
1148 unique_add(self._new_contents, trans_id, 'directory')
1149
1150 def create_symlink(self, target, trans_id):
1151 """Schedule creation of a new symbolic link.
1152
1153 target is a bytestring.
1154 See also new_symlink.
1155 """
1156 if has_symlinks():
1157 os.symlink(target, self._limbo_name(trans_id))
1158 unique_add(self._new_contents, trans_id, 'symlink')
1159 else:
1160 try:
1161 path = FinalPaths(self).get_path(trans_id)
1162 except KeyError:
1163 path = None
1164 raise UnableCreateSymlink(path=path)
1165
1166 def cancel_creation(self, trans_id):
1167 """Cancel the creation of new file contents."""
1168 del self._new_contents[trans_id]
1169 children = self._limbo_children.get(trans_id)
1170 # if this is a limbo directory with children, move them before removing
1171 # the directory
1172 if children is not None:
1173 self._rename_in_limbo(children)
1174 del self._limbo_children[trans_id]
1175 del self._limbo_children_names[trans_id]
1176 delete_any(self._limbo_name(trans_id))
1177
1178
1179class TreeTransform(DiskTreeTransform):
1231 """Represent a tree transformation.1180 """Represent a tree transformation.
12321181
1233 This object is designed to support incremental generation of the transform,1182 This object is designed to support incremental generation of the transform,
@@ -1319,10 +1268,96 @@
1319 tree.unlock()1268 tree.unlock()
1320 raise1269 raise
13211270
1322 TreeTransformBase.__init__(self, tree, limbodir, pb,1271 # Cache of realpath results, to speed up canonical_path
1272 self._realpaths = {}
1273 # Cache of relpath results, to speed up canonical_path
1274 self._relpaths = {}
1275 DiskTreeTransform.__init__(self, tree, limbodir, pb,
1323 tree.case_sensitive)1276 tree.case_sensitive)
1324 self._deletiondir = deletiondir1277 self._deletiondir = deletiondir
13251278
1279 def canonical_path(self, path):
1280 """Get the canonical tree-relative path"""
1281 # don't follow final symlinks
1282 abs = self._tree.abspath(path)
1283 if abs in self._relpaths:
1284 return self._relpaths[abs]
1285 dirname, basename = os.path.split(abs)
1286 if dirname not in self._realpaths:
1287 self._realpaths[dirname] = os.path.realpath(dirname)
1288 dirname = self._realpaths[dirname]
1289 abs = pathjoin(dirname, basename)
1290 if dirname in self._relpaths:
1291 relpath = pathjoin(self._relpaths[dirname], basename)
1292 relpath = relpath.rstrip('/\\')
1293 else:
1294 relpath = self._tree.relpath(abs)
1295 self._relpaths[abs] = relpath
1296 return relpath
1297
1298 def tree_kind(self, trans_id):
1299 """Determine the file kind in the working tree.
1300
1301 Raises NoSuchFile if the file does not exist
1302 """
1303 path = self._tree_id_paths.get(trans_id)
1304 if path is None:
1305 raise NoSuchFile(None)
1306 try:
1307 return file_kind(self._tree.abspath(path))
1308 except OSError, e:
1309 if e.errno != errno.ENOENT:
1310 raise
1311 else:
1312 raise NoSuchFile(path)
1313
1314 def _set_mode(self, trans_id, mode_id, typefunc):
1315 """Set the mode of new file contents.
1316 The mode_id is the existing file to get the mode from (often the same
1317 as trans_id). The operation is only performed if there's a mode match
1318 according to typefunc.
1319 """
1320 if mode_id is None:
1321 mode_id = trans_id
1322 try:
1323 old_path = self._tree_id_paths[mode_id]
1324 except KeyError:
1325 return
1326 try:
1327 mode = os.stat(self._tree.abspath(old_path)).st_mode
1328 except OSError, e:
1329 if e.errno in (errno.ENOENT, errno.ENOTDIR):
1330 # Either old_path doesn't exist, or the parent of the
1331 # target is not a directory (but will be one eventually)
1332 # Either way, we know it doesn't exist *right now*
1333 # See also bug #248448
1334 return
1335 else:
1336 raise
1337 if typefunc(mode):
1338 os.chmod(self._limbo_name(trans_id), mode)
1339
1340 def iter_tree_children(self, parent_id):
1341 """Iterate through the entry's tree children, if any"""
1342 try:
1343 path = self._tree_id_paths[parent_id]
1344 except KeyError:
1345 return
1346 try:
1347 children = os.listdir(self._tree.abspath(path))
1348 except OSError, e:
1349 if not (osutils._is_error_enotdir(e)
1350 or e.errno in (errno.ENOENT, errno.ESRCH)):
1351 raise
1352 return
1353
1354 for child in children:
1355 childpath = joinpath(path, child)
1356 if self._tree.is_control_filename(childpath):
1357 continue
1358 yield self.trans_id_tree_path(childpath)
1359
1360
1326 def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):1361 def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1327 """Apply all changes to the inventory and filesystem.1362 """Apply all changes to the inventory and filesystem.
13281363
@@ -1505,7 +1540,7 @@
1505 return modified_paths1540 return modified_paths
15061541
15071542
1508class TransformPreview(TreeTransformBase):1543class TransformPreview(DiskTreeTransform):
1509 """A TreeTransform for generating preview trees.1544 """A TreeTransform for generating preview trees.
15101545
1511 Unlike TreeTransform, this version works when the input tree is a1546 Unlike TreeTransform, this version works when the input tree is a
@@ -1516,7 +1551,7 @@
1516 def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):1551 def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
1517 tree.lock_read()1552 tree.lock_read()
1518 limbodir = osutils.mkdtemp(prefix='bzr-limbo-')1553 limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1519 TreeTransformBase.__init__(self, tree, limbodir, pb, case_sensitive)1554 DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
15201555
1521 def canonical_path(self, path):1556 def canonical_path(self, path):
1522 return path1557 return path