Merge lp:~jelmer/brz/transform2 into lp:brz/3.1

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz/transform2
Merge into: lp:brz/3.1
Diff against target: 1446 lines (+852/-413)
4 files modified
breezy/bzr/transform.py (+222/-3)
breezy/git/transform.py (+222/-4)
breezy/tests/per_workingtree/test_transform.py (+4/-6)
breezy/transform.py (+404/-400)
To merge this branch: bzr merge lp:~jelmer/brz/transform2
Reviewer Review Type Date Requested Status
Jelmer Vernooij Approve
Review via email: mp+386860@code.launchpad.net

Commit message

Rationalize TreeTransform class hierarchy.

Description of the change

Rationalize TreeTransform class hierarchy.

Add a common base class with non-file-id functions.

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'breezy/bzr/transform.py'
--- breezy/bzr/transform.py 2020-07-05 13:18:03 +0000
+++ breezy/bzr/transform.py 2020-07-05 19:03:19 +0000
@@ -29,6 +29,7 @@
29 revision as _mod_revision,29 revision as _mod_revision,
30 tree,30 tree,
31 ui,31 ui,
32 urlutils,
32 )33 )
3334
34from ..i18n import gettext35from ..i18n import gettext
@@ -36,7 +37,6 @@
36from ..sixish import text_type, viewvalues, viewitems37from ..sixish import text_type, viewvalues, viewitems
37from ..transform import (38from ..transform import (
38 ROOT_PARENT,39 ROOT_PARENT,
39 TreeTransform,
40 _FileMover,40 _FileMover,
41 _TransformResults,41 _TransformResults,
42 DiskTreeTransform,42 DiskTreeTransform,
@@ -52,8 +52,226 @@
52 )52 )
5353
5454
55class InventoryTreeTransform(TreeTransform):55class InventoryTreeTransform(DiskTreeTransform):
56 """Tree transform for Bazaar trees."""56 """Represent a tree transformation.
57
58 This object is designed to support incremental generation of the transform,
59 in any order.
60
61 However, it gives optimum performance when parent directories are created
62 before their contents. The transform is then able to put child files
63 directly in their parent directory, avoiding later renames.
64
65 It is easy to produce malformed transforms, but they are generally
66 harmless. Attempting to apply a malformed transform will cause an
67 exception to be raised before any modifications are made to the tree.
68
69 Many kinds of malformed transforms can be corrected with the
70 resolve_conflicts function. The remaining ones indicate programming error,
71 such as trying to create a file with no path.
72
73 Two sets of file creation methods are supplied. Convenience methods are:
74 * new_file
75 * new_directory
76 * new_symlink
77
78 These are composed of the low-level methods:
79 * create_path
80 * create_file or create_directory or create_symlink
81 * version_file
82 * set_executability
83
84 Transform/Transaction ids
85 -------------------------
86 trans_ids are temporary ids assigned to all files involved in a transform.
87 It's possible, even common, that not all files in the Tree have trans_ids.
88
89 trans_ids are used because filenames and file_ids are not good enough
90 identifiers; filenames change, and not all files have file_ids. File-ids
91 are also associated with trans-ids, so that moving a file moves its
92 file-id.
93
94 trans_ids are only valid for the TreeTransform that generated them.
95
96 Limbo
97 -----
98 Limbo is a temporary directory use to hold new versions of files.
99 Files are added to limbo by create_file, create_directory, create_symlink,
100 and their convenience variants (new_*). Files may be removed from limbo
101 using cancel_creation. Files are renamed from limbo into their final
102 location as part of TreeTransform.apply
103
104 Limbo must be cleaned up, by either calling TreeTransform.apply or
105 calling TreeTransform.finalize.
106
107 Files are placed into limbo inside their parent directories, where
108 possible. This reduces subsequent renames, and makes operations involving
109 lots of files faster. This optimization is only possible if the parent
110 directory is created *before* creating any of its children, so avoid
111 creating children before parents, where possible.
112
113 Pending-deletion
114 ----------------
115 This temporary directory is used by _FileMover for storing files that are
116 about to be deleted. In case of rollback, the files will be restored.
117 FileMover does not delete files until it is sure that a rollback will not
118 happen.
119 """
120
121 def __init__(self, tree, pb=None):
122 """Note: a tree_write lock is taken on the tree.
123
124 Use TreeTransform.finalize() to release the lock (can be omitted if
125 TreeTransform.apply() called).
126 """
127 tree.lock_tree_write()
128 try:
129 limbodir = urlutils.local_path_from_url(
130 tree._transport.abspath('limbo'))
131 osutils.ensure_empty_directory_exists(
132 limbodir,
133 errors.ExistingLimbo)
134 deletiondir = urlutils.local_path_from_url(
135 tree._transport.abspath('pending-deletion'))
136 osutils.ensure_empty_directory_exists(
137 deletiondir,
138 errors.ExistingPendingDeletion)
139 except BaseException:
140 tree.unlock()
141 raise
142
143 # Cache of realpath results, to speed up canonical_path
144 self._realpaths = {}
145 # Cache of relpath results, to speed up canonical_path
146 self._relpaths = {}
147 DiskTreeTransform.__init__(self, tree, limbodir, pb,
148 tree.case_sensitive)
149 self._deletiondir = deletiondir
150
151 def canonical_path(self, path):
152 """Get the canonical tree-relative path"""
153 # don't follow final symlinks
154 abs = self._tree.abspath(path)
155 if abs in self._relpaths:
156 return self._relpaths[abs]
157 dirname, basename = os.path.split(abs)
158 if dirname not in self._realpaths:
159 self._realpaths[dirname] = os.path.realpath(dirname)
160 dirname = self._realpaths[dirname]
161 abs = osutils.pathjoin(dirname, basename)
162 if dirname in self._relpaths:
163 relpath = osutils.pathjoin(self._relpaths[dirname], basename)
164 relpath = relpath.rstrip('/\\')
165 else:
166 relpath = self._tree.relpath(abs)
167 self._relpaths[abs] = relpath
168 return relpath
169
170 def tree_kind(self, trans_id):
171 """Determine the file kind in the working tree.
172
173 :returns: The file kind or None if the file does not exist
174 """
175 path = self._tree_id_paths.get(trans_id)
176 if path is None:
177 return None
178 try:
179 return osutils.file_kind(self._tree.abspath(path))
180 except errors.NoSuchFile:
181 return None
182
183 def _set_mode(self, trans_id, mode_id, typefunc):
184 """Set the mode of new file contents.
185 The mode_id is the existing file to get the mode from (often the same
186 as trans_id). The operation is only performed if there's a mode match
187 according to typefunc.
188 """
189 if mode_id is None:
190 mode_id = trans_id
191 try:
192 old_path = self._tree_id_paths[mode_id]
193 except KeyError:
194 return
195 try:
196 mode = os.stat(self._tree.abspath(old_path)).st_mode
197 except OSError as e:
198 if e.errno in (errno.ENOENT, errno.ENOTDIR):
199 # Either old_path doesn't exist, or the parent of the
200 # target is not a directory (but will be one eventually)
201 # Either way, we know it doesn't exist *right now*
202 # See also bug #248448
203 return
204 else:
205 raise
206 if typefunc(mode):
207 osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
208
209 def iter_tree_children(self, parent_id):
210 """Iterate through the entry's tree children, if any"""
211 try:
212 path = self._tree_id_paths[parent_id]
213 except KeyError:
214 return
215 try:
216 children = os.listdir(self._tree.abspath(path))
217 except OSError as e:
218 if not (osutils._is_error_enotdir(e) or
219 e.errno in (errno.ENOENT, errno.ESRCH)):
220 raise
221 return
222
223 for child in children:
224 childpath = joinpath(path, child)
225 if self._tree.is_control_filename(childpath):
226 continue
227 yield self.trans_id_tree_path(childpath)
228
229 def _generate_limbo_path(self, trans_id):
230 """Generate a limbo path using the final path if possible.
231
232 This optimizes the performance of applying the tree transform by
233 avoiding renames. These renames can be avoided only when the parent
234 directory is already scheduled for creation.
235
236 If the final path cannot be used, falls back to using the trans_id as
237 the relpath.
238 """
239 parent = self._new_parent.get(trans_id)
240 # if the parent directory is already in limbo (e.g. when building a
241 # tree), choose a limbo name inside the parent, to reduce further
242 # renames.
243 use_direct_path = False
244 if self._new_contents.get(parent) == 'directory':
245 filename = self._new_name.get(trans_id)
246 if filename is not None:
247 if parent not in self._limbo_children:
248 self._limbo_children[parent] = set()
249 self._limbo_children_names[parent] = {}
250 use_direct_path = True
251 # the direct path can only be used if no other file has
252 # already taken this pathname, i.e. if the name is unused, or
253 # if it is already associated with this trans_id.
254 elif self._case_sensitive_target:
255 if (self._limbo_children_names[parent].get(filename)
256 in (trans_id, None)):
257 use_direct_path = True
258 else:
259 for l_filename, l_trans_id in viewitems(
260 self._limbo_children_names[parent]):
261 if l_trans_id == trans_id:
262 continue
263 if l_filename.lower() == filename.lower():
264 break
265 else:
266 use_direct_path = True
267
268 if not use_direct_path:
269 return DiskTreeTransform._generate_limbo_path(self, trans_id)
270
271 limbo_name = osutils.pathjoin(self._limbo_files[parent], filename)
272 self._limbo_children[parent].add(trans_id)
273 self._limbo_children_names[parent][filename] = trans_id
274 return limbo_name
57275
58 def version_file(self, trans_id, file_id=None):276 def version_file(self, trans_id, file_id=None):
59 """Schedule a file to become versioned."""277 """Schedule a file to become versioned."""
@@ -109,6 +327,7 @@
109 hook(self._tree, self)327 hook(self._tree, self)
110 if not no_conflicts:328 if not no_conflicts:
111 self._check_malformed()329 self._check_malformed()
330 self.rename_count = 0
112 with ui.ui_factory.nested_progress_bar() as child_pb:331 with ui.ui_factory.nested_progress_bar() as child_pb:
113 if precomputed_delta is None:332 if precomputed_delta is None:
114 child_pb.update(gettext('Apply phase'), 0, 2)333 child_pb.update(gettext('Apply phase'), 0, 2)
115334
=== modified file 'breezy/git/transform.py'
--- breezy/git/transform.py 2020-07-05 13:18:03 +0000
+++ breezy/git/transform.py 2020-07-05 19:03:19 +0000
@@ -20,15 +20,16 @@
20import errno20import errno
21import os21import os
2222
23from .. import errors, ui23from .. import errors, osutils, ui, urlutils
24from ..i18n import gettext24from ..i18n import gettext
25from ..mutabletree import MutableTree25from ..mutabletree import MutableTree
26from ..sixish import viewitems26from ..sixish import viewitems
27from ..transform import (27from ..transform import (
28 TreeTransform,28 DiskTreeTransform,
29 _TransformResults,29 _TransformResults,
30 _FileMover,30 _FileMover,
31 FinalPaths,31 FinalPaths,
32 joinpath,
32 unique_add,33 unique_add,
33 TransformRenameFailed,34 TransformRenameFailed,
34 )35 )
@@ -37,8 +38,224 @@
37from ..bzr.transform import TransformPreview as GitTransformPreview38from ..bzr.transform import TransformPreview as GitTransformPreview
3839
3940
40class GitTreeTransform(TreeTransform):41class GitTreeTransform(DiskTreeTransform):
41 """Tree transform for Bazaar trees."""42 """Represent a tree transformation.
43
44 This object is designed to support incremental generation of the transform,
45 in any order.
46
47 However, it gives optimum performance when parent directories are created
48 before their contents. The transform is then able to put child files
49 directly in their parent directory, avoiding later renames.
50
51 It is easy to produce malformed transforms, but they are generally
52 harmless. Attempting to apply a malformed transform will cause an
53 exception to be raised before any modifications are made to the tree.
54
55 Many kinds of malformed transforms can be corrected with the
56 resolve_conflicts function. The remaining ones indicate programming error,
57 such as trying to create a file with no path.
58
59 Two sets of file creation methods are supplied. Convenience methods are:
60 * new_file
61 * new_directory
62 * new_symlink
63
64 These are composed of the low-level methods:
65 * create_path
66 * create_file or create_directory or create_symlink
67 * version_file
68 * set_executability
69
70 Transform/Transaction ids
71 -------------------------
72 trans_ids are temporary ids assigned to all files involved in a transform.
73 It's possible, even common, that not all files in the Tree have trans_ids.
74
75 trans_ids are used because filenames and file_ids are not good enough
76 identifiers; filenames change.
77
78 trans_ids are only valid for the TreeTransform that generated them.
79
80 Limbo
81 -----
82 Limbo is a temporary directory use to hold new versions of files.
83 Files are added to limbo by create_file, create_directory, create_symlink,
84 and their convenience variants (new_*). Files may be removed from limbo
85 using cancel_creation. Files are renamed from limbo into their final
86 location as part of TreeTransform.apply
87
88 Limbo must be cleaned up, by either calling TreeTransform.apply or
89 calling TreeTransform.finalize.
90
91 Files are placed into limbo inside their parent directories, where
92 possible. This reduces subsequent renames, and makes operations involving
93 lots of files faster. This optimization is only possible if the parent
94 directory is created *before* creating any of its children, so avoid
95 creating children before parents, where possible.
96
97 Pending-deletion
98 ----------------
99 This temporary directory is used by _FileMover for storing files that are
100 about to be deleted. In case of rollback, the files will be restored.
101 FileMover does not delete files until it is sure that a rollback will not
102 happen.
103 """
104
105 def __init__(self, tree, pb=None):
106 """Note: a tree_write lock is taken on the tree.
107
108 Use TreeTransform.finalize() to release the lock (can be omitted if
109 TreeTransform.apply() called).
110 """
111 tree.lock_tree_write()
112 try:
113 limbodir = urlutils.local_path_from_url(
114 tree._transport.abspath('limbo'))
115 osutils.ensure_empty_directory_exists(
116 limbodir,
117 errors.ExistingLimbo)
118 deletiondir = urlutils.local_path_from_url(
119 tree._transport.abspath('pending-deletion'))
120 osutils.ensure_empty_directory_exists(
121 deletiondir,
122 errors.ExistingPendingDeletion)
123 except BaseException:
124 tree.unlock()
125 raise
126
127 # Cache of realpath results, to speed up canonical_path
128 self._realpaths = {}
129 # Cache of relpath results, to speed up canonical_path
130 self._relpaths = {}
131 DiskTreeTransform.__init__(self, tree, limbodir, pb,
132 tree.case_sensitive)
133 self._deletiondir = deletiondir
134
135 def canonical_path(self, path):
136 """Get the canonical tree-relative path"""
137 # don't follow final symlinks
138 abs = self._tree.abspath(path)
139 if abs in self._relpaths:
140 return self._relpaths[abs]
141 dirname, basename = os.path.split(abs)
142 if dirname not in self._realpaths:
143 self._realpaths[dirname] = os.path.realpath(dirname)
144 dirname = self._realpaths[dirname]
145 abs = osutils.pathjoin(dirname, basename)
146 if dirname in self._relpaths:
147 relpath = osutils.pathjoin(self._relpaths[dirname], basename)
148 relpath = relpath.rstrip('/\\')
149 else:
150 relpath = self._tree.relpath(abs)
151 self._relpaths[abs] = relpath
152 return relpath
153
154 def tree_kind(self, trans_id):
155 """Determine the file kind in the working tree.
156
157 :returns: The file kind or None if the file does not exist
158 """
159 path = self._tree_id_paths.get(trans_id)
160 if path is None:
161 return None
162 try:
163 return osutils.file_kind(self._tree.abspath(path))
164 except errors.NoSuchFile:
165 return None
166
167 def _set_mode(self, trans_id, mode_id, typefunc):
168 """Set the mode of new file contents.
169 The mode_id is the existing file to get the mode from (often the same
170 as trans_id). The operation is only performed if there's a mode match
171 according to typefunc.
172 """
173 if mode_id is None:
174 mode_id = trans_id
175 try:
176 old_path = self._tree_id_paths[mode_id]
177 except KeyError:
178 return
179 try:
180 mode = os.stat(self._tree.abspath(old_path)).st_mode
181 except OSError as e:
182 if e.errno in (errno.ENOENT, errno.ENOTDIR):
183 # Either old_path doesn't exist, or the parent of the
184 # target is not a directory (but will be one eventually)
185 # Either way, we know it doesn't exist *right now*
186 # See also bug #248448
187 return
188 else:
189 raise
190 if typefunc(mode):
191 osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
192
193 def iter_tree_children(self, parent_id):
194 """Iterate through the entry's tree children, if any"""
195 try:
196 path = self._tree_id_paths[parent_id]
197 except KeyError:
198 return
199 try:
200 children = os.listdir(self._tree.abspath(path))
201 except OSError as e:
202 if not (osutils._is_error_enotdir(e) or
203 e.errno in (errno.ENOENT, errno.ESRCH)):
204 raise
205 return
206
207 for child in children:
208 childpath = joinpath(path, child)
209 if self._tree.is_control_filename(childpath):
210 continue
211 yield self.trans_id_tree_path(childpath)
212
213 def _generate_limbo_path(self, trans_id):
214 """Generate a limbo path using the final path if possible.
215
216 This optimizes the performance of applying the tree transform by
217 avoiding renames. These renames can be avoided only when the parent
218 directory is already scheduled for creation.
219
220 If the final path cannot be used, falls back to using the trans_id as
221 the relpath.
222 """
223 parent = self._new_parent.get(trans_id)
224 # if the parent directory is already in limbo (e.g. when building a
225 # tree), choose a limbo name inside the parent, to reduce further
226 # renames.
227 use_direct_path = False
228 if self._new_contents.get(parent) == 'directory':
229 filename = self._new_name.get(trans_id)
230 if filename is not None:
231 if parent not in self._limbo_children:
232 self._limbo_children[parent] = set()
233 self._limbo_children_names[parent] = {}
234 use_direct_path = True
235 # the direct path can only be used if no other file has
236 # already taken this pathname, i.e. if the name is unused, or
237 # if it is already associated with this trans_id.
238 elif self._case_sensitive_target:
239 if (self._limbo_children_names[parent].get(filename)
240 in (trans_id, None)):
241 use_direct_path = True
242 else:
243 for l_filename, l_trans_id in viewitems(
244 self._limbo_children_names[parent]):
245 if l_trans_id == trans_id:
246 continue
247 if l_filename.lower() == filename.lower():
248 break
249 else:
250 use_direct_path = True
251
252 if not use_direct_path:
253 return DiskTreeTransform._generate_limbo_path(self, trans_id)
254
255 limbo_name = osutils.pathjoin(self._limbo_files[parent], filename)
256 self._limbo_children[parent].add(trans_id)
257 self._limbo_children_names[parent][filename] = trans_id
258 return limbo_name
42259
43 def version_file(self, trans_id, file_id=None):260 def version_file(self, trans_id, file_id=None):
44 """Schedule a file to become versioned."""261 """Schedule a file to become versioned."""
@@ -71,6 +288,7 @@
71 hook(self._tree, self)288 hook(self._tree, self)
72 if not no_conflicts:289 if not no_conflicts:
73 self._check_malformed()290 self._check_malformed()
291 self.rename_count = 0
74 with ui.ui_factory.nested_progress_bar() as child_pb:292 with ui.ui_factory.nested_progress_bar() as child_pb:
75 if precomputed_delta is None:293 if precomputed_delta is None:
76 child_pb.update(gettext('Apply phase'), 0, 2)294 child_pb.update(gettext('Apply phase'), 0, 2)
77295
=== modified file 'breezy/tests/per_workingtree/test_transform.py'
--- breezy/tests/per_workingtree/test_transform.py 2020-07-05 13:18:03 +0000
+++ breezy/tests/per_workingtree/test_transform.py 2020-07-05 19:03:19 +0000
@@ -1326,15 +1326,13 @@
1326 def test_rename_count(self):1326 def test_rename_count(self):
1327 transform, root = self.transform()1327 transform, root = self.transform()
1328 transform.new_file('name1', root, [b'contents'])1328 transform.new_file('name1', root, [b'contents'])
1329 self.assertEqual(transform.rename_count, 0)1329 result = transform.apply()
1330 transform.apply()1330 self.assertEqual(result.rename_count, 1)
1331 self.assertEqual(transform.rename_count, 1)
1332 transform2, root = self.transform()1331 transform2, root = self.transform()
1333 transform2.adjust_path('name2', root,1332 transform2.adjust_path('name2', root,
1334 transform2.trans_id_tree_path('name1'))1333 transform2.trans_id_tree_path('name1'))
1335 self.assertEqual(transform2.rename_count, 0)1334 result = transform2.apply()
1336 transform2.apply()1335 self.assertEqual(result.rename_count, 2)
1337 self.assertEqual(transform2.rename_count, 2)
13381336
1339 def test_change_parent(self):1337 def test_change_parent(self):
1340 """Ensure that after we change a parent, the results are still right.1338 """Ensure that after we change a parent, the results are still right.
13411339
=== modified file 'breezy/transform.py'
--- breezy/transform.py 2020-07-05 13:18:03 +0000
+++ breezy/transform.py 2020-07-05 19:03:19 +0000
@@ -126,66 +126,75 @@
126126
127127
128class _TransformResults(object):128class _TransformResults(object):
129
129 def __init__(self, modified_paths, rename_count):130 def __init__(self, modified_paths, rename_count):
130 object.__init__(self)131 object.__init__(self)
131 self.modified_paths = modified_paths132 self.modified_paths = modified_paths
132 self.rename_count = rename_count133 self.rename_count = rename_count
133134
134135
135class TreeTransformBase(object):136class TreeTransform(object):
136 """The base class for TreeTransform and its kin."""137 """Represent a tree transformation.
137138
138 def __init__(self, tree, pb=None, case_sensitive=True):139 This object is designed to support incremental generation of the transform,
139 """Constructor.140 in any order.
140141
141 :param tree: The tree that will be transformed, but not necessarily142 However, it gives optimum performance when parent directories are created
142 the output tree.143 before their contents. The transform is then able to put child files
143 :param pb: ignored144 directly in their parent directory, avoiding later renames.
144 :param case_sensitive: If True, the target of the transform is145
145 case sensitive, not just case preserving.146 It is easy to produce malformed transforms, but they are generally
146 """147 harmless. Attempting to apply a malformed transform will cause an
147 object.__init__(self)148 exception to be raised before any modifications are made to the tree.
149
150 Many kinds of malformed transforms can be corrected with the
151 resolve_conflicts function. The remaining ones indicate programming error,
152 such as trying to create a file with no path.
153
154 Two sets of file creation methods are supplied. Convenience methods are:
155 * new_file
156 * new_directory
157 * new_symlink
158
159 These are composed of the low-level methods:
160 * create_path
161 * create_file or create_directory or create_symlink
162 * version_file
163 * set_executability
164
165 Transform/Transaction ids
166 -------------------------
167 trans_ids are temporary ids assigned to all files involved in a transform.
168 It's possible, even common, that not all files in the Tree have trans_ids.
169
170 trans_ids are only valid for the TreeTransform that generated them.
171 """
172
173 def __init__(self, tree, pb=None):
148 self._tree = tree174 self._tree = tree
175 # A progress bar
176 self._pb = pb
149 self._id_number = 0177 self._id_number = 0
178 # Mapping of path in old tree -> trans_id
179 self._tree_path_ids = {}
180 # Mapping trans_id -> path in old tree
181 self._tree_id_paths = {}
150 # mapping of trans_id -> new basename182 # mapping of trans_id -> new basename
151 self._new_name = {}183 self._new_name = {}
152 # mapping of trans_id -> new parent trans_id184 # mapping of trans_id -> new parent trans_id
153 self._new_parent = {}185 self._new_parent = {}
154 # mapping of trans_id with new contents -> new file_kind186 # mapping of trans_id with new contents -> new file_kind
155 self._new_contents = {}187 self._new_contents = {}
156 # mapping of trans_id => (sha1 of content, stat_value)
157 self._observed_sha1s = {}
158 # Set of trans_ids whose contents will be removed188 # Set of trans_ids whose contents will be removed
159 self._removed_contents = set()189 self._removed_contents = set()
160 # Mapping of trans_id -> new execute-bit value190 # Mapping of trans_id -> new execute-bit value
161 self._new_executability = {}191 self._new_executability = {}
162 # Mapping of trans_id -> new tree-reference value192 # Mapping of trans_id -> new tree-reference value
163 self._new_reference_revision = {}193 self._new_reference_revision = {}
164 # Mapping of trans_id -> new file_id
165 self._new_id = {}
166 # Mapping of old file-id -> trans_id
167 self._non_present_ids = {}
168 # Mapping of new file_id -> trans_id
169 self._r_new_id = {}
170 # Set of trans_ids that will be removed194 # Set of trans_ids that will be removed
171 self._removed_id = set()195 self._removed_id = set()
172 # Mapping of path in old tree -> trans_id
173 self._tree_path_ids = {}
174 # Mapping trans_id -> path in old tree
175 self._tree_id_paths = {}
176 # The trans_id that will be used as the tree root
177 if tree.is_versioned(''):
178 self._new_root = self.trans_id_tree_path('')
179 else:
180 self._new_root = None
181 # Indicator of whether the transform has been applied196 # Indicator of whether the transform has been applied
182 self._done = False197 self._done = False
183 # A progress bar
184 self._pb = pb
185 # Whether the target is case sensitive
186 self._case_sensitive_target = case_sensitive
187 # A counter of how many files have been renamed
188 self.rename_count = 0
189198
190 def __enter__(self):199 def __enter__(self):
191 """Support Context Manager API."""200 """Support Context Manager API."""
@@ -203,15 +212,354 @@
203 """212 """
204 raise NotImplementedError(self.iter_tree_children)213 raise NotImplementedError(self.iter_tree_children)
205214
206 def _read_symlink_target(self, trans_id):
207 raise NotImplementedError(self._read_symlink_target)
208
209 def canonical_path(self, path):215 def canonical_path(self, path):
210 return path216 return path
211217
212 def tree_kind(self, trans_id):218 def tree_kind(self, trans_id):
213 raise NotImplementedError(self.tree_kind)219 raise NotImplementedError(self.tree_kind)
214220
221 def by_parent(self):
222 """Return a map of parent: children for known parents.
223
224 Only new paths and parents of tree files with assigned ids are used.
225 """
226 by_parent = {}
227 items = list(viewitems(self._new_parent))
228 items.extend((t, self.final_parent(t))
229 for t in list(self._tree_id_paths))
230 for trans_id, parent_id in items:
231 if parent_id not in by_parent:
232 by_parent[parent_id] = set()
233 by_parent[parent_id].add(trans_id)
234 return by_parent
235
236 def finalize(self):
237 """Release the working tree lock, if held.
238
239 This is required if apply has not been invoked, but can be invoked
240 even after apply.
241 """
242 raise NotImplementedError(self.finalize)
243
244 def create_path(self, name, parent):
245 """Assign a transaction id to a new path"""
246 trans_id = self._assign_id()
247 unique_add(self._new_name, trans_id, name)
248 unique_add(self._new_parent, trans_id, parent)
249 return trans_id
250
251 def adjust_path(self, name, parent, trans_id):
252 """Change the path that is assigned to a transaction id."""
253 if parent is None:
254 raise ValueError("Parent trans-id may not be None")
255 if trans_id == self._new_root:
256 raise CantMoveRoot
257 self._new_name[trans_id] = name
258 self._new_parent[trans_id] = parent
259
260 def adjust_root_path(self, name, parent):
261 """Emulate moving the root by moving all children, instead.
262
263 We do this by undoing the association of root's transaction id with the
264 current tree. This allows us to create a new directory with that
265 transaction id. We unversion the root directory and version the
266 physically new directory, and hope someone versions the tree root
267 later.
268 """
269 raise NotImplementedError(self.adjust_root_path)
270
271 def fixup_new_roots(self):
272 """Reinterpret requests to change the root directory
273
274 Instead of creating a root directory, or moving an existing directory,
275 all the attributes and children of the new root are applied to the
276 existing root directory.
277
278 This means that the old root trans-id becomes obsolete, so it is
279 recommended only to invoke this after the root trans-id has become
280 irrelevant.
281 """
282 raise NotImplementedError(self.fixup_new_roots)
283
284 def _assign_id(self):
285 """Produce a new tranform id"""
286 new_id = "new-%s" % self._id_number
287 self._id_number += 1
288 return new_id
289
290 def trans_id_tree_path(self, path):
291 """Determine (and maybe set) the transaction ID for a tree path."""
292 path = self.canonical_path(path)
293 if path not in self._tree_path_ids:
294 self._tree_path_ids[path] = self._assign_id()
295 self._tree_id_paths[self._tree_path_ids[path]] = path
296 return self._tree_path_ids[path]
297
298 def get_tree_parent(self, trans_id):
299 """Determine id of the parent in the tree."""
300 path = self._tree_id_paths[trans_id]
301 if path == "":
302 return ROOT_PARENT
303 return self.trans_id_tree_path(os.path.dirname(path))
304
305 def delete_contents(self, trans_id):
306 """Schedule the contents of a path entry for deletion"""
307 kind = self.tree_kind(trans_id)
308 if kind is not None:
309 self._removed_contents.add(trans_id)
310
311 def cancel_deletion(self, trans_id):
312 """Cancel a scheduled deletion"""
313 self._removed_contents.remove(trans_id)
314
315 def delete_versioned(self, trans_id):
316 """Delete and unversion a versioned file"""
317 self.delete_contents(trans_id)
318 self.unversion_file(trans_id)
319
320 def set_executability(self, executability, trans_id):
321 """Schedule setting of the 'execute' bit
322 To unschedule, set to None
323 """
324 if executability is None:
325 del self._new_executability[trans_id]
326 else:
327 unique_add(self._new_executability, trans_id, executability)
328
329 def set_tree_reference(self, revision_id, trans_id):
330 """Set the reference associated with a directory"""
331 unique_add(self._new_reference_revision, trans_id, revision_id)
332
333 def version_file(self, trans_id, file_id=None):
334 """Schedule a file to become versioned."""
335 raise NotImplementedError(self.version_file)
336
337 def cancel_versioning(self, trans_id):
338 """Undo a previous versioning of a file"""
339 raise NotImplementedError(self.cancel_versioning)
340
341 def unversion_file(self, trans_id):
342 """Schedule a path entry to become unversioned"""
343 self._removed_id.add(trans_id)
344
345 def new_paths(self, filesystem_only=False):
346 """Determine the paths of all new and changed files.
347
348 :param filesystem_only: if True, only calculate values for files
349 that require renames or execute bit changes.
350 """
351 raise NotImplementedError(self.new_paths)
352
353 def final_kind(self, trans_id):
354 """Determine the final file kind, after any changes applied.
355
356 :return: None if the file does not exist/has no contents. (It is
357 conceivable that a path would be created without the corresponding
358 contents insertion command)
359 """
360 if trans_id in self._new_contents:
361 return self._new_contents[trans_id]
362 elif trans_id in self._removed_contents:
363 return None
364 else:
365 return self.tree_kind(trans_id)
366
367 def tree_path(self, trans_id):
368 """Determine the tree path associated with the trans_id."""
369 return self._tree_id_paths.get(trans_id)
370
371 def final_is_versioned(self, trans_id):
372 raise NotImplementedError(self.final_is_versioned)
373
374 def final_parent(self, trans_id):
375 """Determine the parent file_id, after any changes are applied.
376
377 ROOT_PARENT is returned for the tree root.
378 """
379 try:
380 return self._new_parent[trans_id]
381 except KeyError:
382 return self.get_tree_parent(trans_id)
383
384 def final_name(self, trans_id):
385 """Determine the final filename, after all changes are applied."""
386 try:
387 return self._new_name[trans_id]
388 except KeyError:
389 try:
390 return os.path.basename(self._tree_id_paths[trans_id])
391 except KeyError:
392 raise NoFinalPath(trans_id, self)
393
394 def path_changed(self, trans_id):
395 """Return True if a trans_id's path has changed."""
396 return (trans_id in self._new_name) or (trans_id in self._new_parent)
397
398 def new_contents(self, trans_id):
399 return (trans_id in self._new_contents)
400
401 def find_conflicts(self):
402 """Find any violations of inventory or filesystem invariants"""
403 raise NotImplementedError(self.find_conflicts)
404
405 def new_file(self, name, parent_id, contents, file_id=None,
406 executable=None, sha1=None):
407 """Convenience method to create files.
408
409 name is the name of the file to create.
410 parent_id is the transaction id of the parent directory of the file.
411 contents is an iterator of bytestrings, which will be used to produce
412 the file.
413 :param file_id: The inventory ID of the file, if it is to be versioned.
414 :param executable: Only valid when a file_id has been supplied.
415 """
416 raise NotImplementedError(self.new_file)
417
418 def new_directory(self, name, parent_id, file_id=None):
419 """Convenience method to create directories.
420
421 name is the name of the directory to create.
422 parent_id is the transaction id of the parent directory of the
423 directory.
424 file_id is the inventory ID of the directory, if it is to be versioned.
425 """
426 raise NotImplementedError(self.new_directory)
427
428 def new_symlink(self, name, parent_id, target, file_id=None):
429 """Convenience method to create symbolic link.
430
431 name is the name of the symlink to create.
432 parent_id is the transaction id of the parent directory of the symlink.
433 target is a bytestring of the target of the symlink.
434 file_id is the inventory ID of the file, if it is to be versioned.
435 """
436 raise NotImplementedError(self.new_symlink)
437
438 def new_orphan(self, trans_id, parent_id):
439 """Schedule an item to be orphaned.
440
441 When a directory is about to be removed, its children, if they are not
442 versioned are moved out of the way: they don't have a parent anymore.
443
444 :param trans_id: The trans_id of the existing item.
445 :param parent_id: The parent trans_id of the item.
446 """
447 raise NotImplementedError(self.new_orphan)
448
449 def iter_changes(self):
450 """Produce output in the same format as Tree.iter_changes.
451
452 Will produce nonsensical results if invoked while inventory/filesystem
453 conflicts (as reported by TreeTransform.find_conflicts()) are present.
454
455 This reads the Transform, but only reproduces changes involving a
456 file_id. Files that are not versioned in either of the FROM or TO
457 states are not reflected.
458 """
459 raise NotImplementedError(self.iter_changes)
460
461 def get_preview_tree(self):
462 """Return a tree representing the result of the transform.
463
464 The tree is a snapshot, and altering the TreeTransform will invalidate
465 it.
466 """
467 raise NotImplementedError(self.get_preview_tree)
468
469 def commit(self, branch, message, merge_parents=None, strict=False,
470 timestamp=None, timezone=None, committer=None, authors=None,
471 revprops=None, revision_id=None):
472 """Commit the result of this TreeTransform to a branch.
473
474 :param branch: The branch to commit to.
475 :param message: The message to attach to the commit.
476 :param merge_parents: Additional parent revision-ids specified by
477 pending merges.
478 :param strict: If True, abort the commit if there are unversioned
479 files.
480 :param timestamp: if not None, seconds-since-epoch for the time and
481 date. (May be a float.)
482 :param timezone: Optional timezone for timestamp, as an offset in
483 seconds.
484 :param committer: Optional committer in email-id format.
485 (e.g. "J Random Hacker <jrandom@example.com>")
486 :param authors: Optional list of authors in email-id format.
487 :param revprops: Optional dictionary of revision properties.
488 :param revision_id: Optional revision id. (Specifying a revision-id
489 may reduce performance for some non-native formats.)
490 :return: The revision_id of the revision committed.
491 """
492 raise NotImplementedError(self.commit)
493
494 def create_file(self, contents, trans_id, mode_id=None, sha1=None):
495 """Schedule creation of a new file.
496
497 :seealso: new_file.
498
499 :param contents: an iterator of strings, all of which will be written
500 to the target destination.
501 :param trans_id: TreeTransform handle
502 :param mode_id: If not None, force the mode of the target file to match
503 the mode of the object referenced by mode_id.
504 Otherwise, we will try to preserve mode bits of an existing file.
505 :param sha1: If the sha1 of this content is already known, pass it in.
506 We can use it to prevent future sha1 computations.
507 """
508 raise NotImplementedError(self.create_file)
509
510 def create_directory(self, trans_id):
511 """Schedule creation of a new directory.
512
513 See also new_directory.
514 """
515 raise NotImplementedError(self.create_directory)
516
517 def create_symlink(self, target, trans_id):
518 """Schedule creation of a new symbolic link.
519
520 target is a bytestring.
521 See also new_symlink.
522 """
523 raise NotImplementedError(self.create_symlink)
524
525 def create_hardlink(self, path, trans_id):
526 """Schedule creation of a hard link"""
527 raise NotImplementedError(self.create_hardlink)
528
529 def cancel_creation(self, trans_id):
530 """Cancel the creation of new file contents."""
531 raise NotImplementedError(self.cancel_creation)
532
533
534class TreeTransformBase(TreeTransform):
535 """The base class for TreeTransform and its kin."""
536
537 def __init__(self, tree, pb=None, case_sensitive=True):
538 """Constructor.
539
540 :param tree: The tree that will be transformed, but not necessarily
541 the output tree.
542 :param pb: ignored
543 :param case_sensitive: If True, the target of the transform is
544 case sensitive, not just case preserving.
545 """
546 super(TreeTransformBase, self).__init__(tree, pb=pb)
547 # mapping of trans_id => (sha1 of content, stat_value)
548 self._observed_sha1s = {}
549 # Mapping of trans_id -> new file_id
550 self._new_id = {}
551 # Mapping of old file-id -> trans_id
552 self._non_present_ids = {}
553 # Mapping of new file_id -> trans_id
554 self._r_new_id = {}
555 # The trans_id that will be used as the tree root
556 if tree.is_versioned(''):
557 self._new_root = self.trans_id_tree_path('')
558 else:
559 self._new_root = None
560 # Whether the target is case sensitive
561 self._case_sensitive_target = case_sensitive
562
215 def finalize(self):563 def finalize(self):
216 """Release the working tree lock, if held.564 """Release the working tree lock, if held.
217565
@@ -230,12 +578,6 @@
230578
231 root = property(__get_root)579 root = property(__get_root)
232580
233 def _assign_id(self):
234 """Produce a new tranform id"""
235 new_id = "new-%s" % self._id_number
236 self._id_number += 1
237 return new_id
238
239 def create_path(self, name, parent):581 def create_path(self, name, parent):
240 """Assign a transaction id to a new path"""582 """Assign a transaction id to a new path"""
241 trans_id = self._assign_id()583 trans_id = self._assign_id()
@@ -243,15 +585,6 @@
243 unique_add(self._new_parent, trans_id, parent)585 unique_add(self._new_parent, trans_id, parent)
244 return trans_id586 return trans_id
245587
246 def adjust_path(self, name, parent, trans_id):
247 """Change the path that is assigned to a transaction id."""
248 if parent is None:
249 raise ValueError("Parent trans-id may not be None")
250 if trans_id == self._new_root:
251 raise CantMoveRoot
252 self._new_name[trans_id] = name
253 self._new_parent[trans_id] = parent
254
255 def adjust_root_path(self, name, parent):588 def adjust_root_path(self, name, parent):
256 """Emulate moving the root by moving all children, instead.589 """Emulate moving the root by moving all children, instead.
257590
@@ -368,53 +701,6 @@
368 else:701 else:
369 return self.trans_id_tree_path(path)702 return self.trans_id_tree_path(path)
370703
371 def trans_id_tree_path(self, path):
372 """Determine (and maybe set) the transaction ID for a tree path."""
373 path = self.canonical_path(path)
374 if path not in self._tree_path_ids:
375 self._tree_path_ids[path] = self._assign_id()
376 self._tree_id_paths[self._tree_path_ids[path]] = path
377 return self._tree_path_ids[path]
378
379 def get_tree_parent(self, trans_id):
380 """Determine id of the parent in the tree."""
381 path = self._tree_id_paths[trans_id]
382 if path == "":
383 return ROOT_PARENT
384 return self.trans_id_tree_path(os.path.dirname(path))
385
386 def delete_contents(self, trans_id):
387 """Schedule the contents of a path entry for deletion"""
388 kind = self.tree_kind(trans_id)
389 if kind is not None:
390 self._removed_contents.add(trans_id)
391
392 def cancel_deletion(self, trans_id):
393 """Cancel a scheduled deletion"""
394 self._removed_contents.remove(trans_id)
395
396 def unversion_file(self, trans_id):
397 """Schedule a path entry to become unversioned"""
398 self._removed_id.add(trans_id)
399
400 def delete_versioned(self, trans_id):
401 """Delete and unversion a versioned file"""
402 self.delete_contents(trans_id)
403 self.unversion_file(trans_id)
404
405 def set_executability(self, executability, trans_id):
406 """Schedule setting of the 'execute' bit
407 To unschedule, set to None
408 """
409 if executability is None:
410 del self._new_executability[trans_id]
411 else:
412 unique_add(self._new_executability, trans_id, executability)
413
414 def set_tree_reference(self, revision_id, trans_id):
415 """Set the reference associated with a directory"""
416 unique_add(self._new_reference_revision, trans_id, revision_id)
417
418 def version_file(self, trans_id, file_id=None):704 def version_file(self, trans_id, file_id=None):
419 """Schedule a file to become versioned."""705 """Schedule a file to become versioned."""
420 raise NotImplementedError(self.version_file)706 raise NotImplementedError(self.version_file)
@@ -444,24 +730,6 @@
444 new_ids.update(id_set)730 new_ids.update(id_set)
445 return sorted(FinalPaths(self).get_paths(new_ids))731 return sorted(FinalPaths(self).get_paths(new_ids))
446732
447 def final_kind(self, trans_id):
448 """Determine the final file kind, after any changes applied.
449
450 :return: None if the file does not exist/has no contents. (It is
451 conceivable that a path would be created without the corresponding
452 contents insertion command)
453 """
454 if trans_id in self._new_contents:
455 return self._new_contents[trans_id]
456 elif trans_id in self._removed_contents:
457 return None
458 else:
459 return self.tree_kind(trans_id)
460
461 def tree_path(self, trans_id):
462 """Determine the tree path associated with the trans_id."""
463 return self._tree_id_paths.get(trans_id)
464
465 def tree_file_id(self, trans_id):733 def tree_file_id(self, trans_id):
466 """Determine the file id associated with the trans_id in the tree"""734 """Determine the file id associated with the trans_id in the tree"""
467 path = self.tree_path(trans_id)735 path = self.tree_path(trans_id)
@@ -500,48 +768,6 @@
500 if value == trans_id:768 if value == trans_id:
501 return key769 return key
502770
503 def final_parent(self, trans_id):
504 """Determine the parent file_id, after any changes are applied.
505
506 ROOT_PARENT is returned for the tree root.
507 """
508 try:
509 return self._new_parent[trans_id]
510 except KeyError:
511 return self.get_tree_parent(trans_id)
512
513 def final_name(self, trans_id):
514 """Determine the final filename, after all changes are applied."""
515 try:
516 return self._new_name[trans_id]
517 except KeyError:
518 try:
519 return os.path.basename(self._tree_id_paths[trans_id])
520 except KeyError:
521 raise NoFinalPath(trans_id, self)
522
523 def by_parent(self):
524 """Return a map of parent: children for known parents.
525
526 Only new paths and parents of tree files with assigned ids are used.
527 """
528 by_parent = {}
529 items = list(viewitems(self._new_parent))
530 items.extend((t, self.final_parent(t))
531 for t in list(self._tree_id_paths))
532 for trans_id, parent_id in items:
533 if parent_id not in by_parent:
534 by_parent[parent_id] = set()
535 by_parent[parent_id].add(trans_id)
536 return by_parent
537
538 def path_changed(self, trans_id):
539 """Return True if a trans_id's path has changed."""
540 return (trans_id in self._new_name) or (trans_id in self._new_parent)
541
542 def new_contents(self, trans_id):
543 return (trans_id in self._new_contents)
544
545 def find_conflicts(self):771 def find_conflicts(self):
546 """Find any violations of inventory or filesystem invariants"""772 """Find any violations of inventory or filesystem invariants"""
547 if self._done is True:773 if self._done is True:
@@ -1240,6 +1466,22 @@
1240 """Cancel the creation of new file contents."""1466 """Cancel the creation of new file contents."""
1241 raise NotImplementedError(self.cancel_creation)1467 raise NotImplementedError(self.cancel_creation)
12421468
1469 def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1470 """Apply all changes to the inventory and filesystem.
1471
1472 If filesystem or inventory conflicts are present, MalformedTransform
1473 will be thrown.
1474
1475 If apply succeeds, finalize is not necessary.
1476
1477 :param no_conflicts: if True, the caller guarantees there are no
1478 conflicts, so no check is made.
1479 :param precomputed_delta: An inventory delta to use instead of
1480 calculating one.
1481 :param _mover: Supply an alternate FileMover, for testing
1482 """
1483 raise NotImplementedError(self.apply)
1484
12431485
1244class DiskTreeTransform(TreeTransformBase):1486class DiskTreeTransform(TreeTransformBase):
1245 """Tree transform storing its contents on disk."""1487 """Tree transform storing its contents on disk."""
@@ -1328,7 +1570,7 @@
1328 def adjust_path(self, name, parent, trans_id):1570 def adjust_path(self, name, parent, trans_id):
1329 previous_parent = self._new_parent.get(trans_id)1571 previous_parent = self._new_parent.get(trans_id)
1330 previous_name = self._new_name.get(trans_id)1572 previous_name = self._new_name.get(trans_id)
1331 TreeTransformBase.adjust_path(self, name, parent, trans_id)1573 super(DiskTreeTransform, self).adjust_path(name, parent, trans_id)
1332 if (trans_id in self._limbo_files1574 if (trans_id in self._limbo_files
1333 and trans_id not in self._needs_rename):1575 and trans_id not in self._needs_rename):
1334 self._rename_in_limbo([trans_id])1576 self._rename_in_limbo([trans_id])
@@ -1546,244 +1788,6 @@
1546 invalid='warning')1788 invalid='warning')
15471789
15481790
1549class TreeTransform(DiskTreeTransform):
1550 """Represent a tree transformation.
1551
1552 This object is designed to support incremental generation of the transform,
1553 in any order.
1554
1555 However, it gives optimum performance when parent directories are created
1556 before their contents. The transform is then able to put child files
1557 directly in their parent directory, avoiding later renames.
1558
1559 It is easy to produce malformed transforms, but they are generally
1560 harmless. Attempting to apply a malformed transform will cause an
1561 exception to be raised before any modifications are made to the tree.
1562
1563 Many kinds of malformed transforms can be corrected with the
1564 resolve_conflicts function. The remaining ones indicate programming error,
1565 such as trying to create a file with no path.
1566
1567 Two sets of file creation methods are supplied. Convenience methods are:
1568 * new_file
1569 * new_directory
1570 * new_symlink
1571
1572 These are composed of the low-level methods:
1573 * create_path
1574 * create_file or create_directory or create_symlink
1575 * version_file
1576 * set_executability
1577
1578 Transform/Transaction ids
1579 -------------------------
1580 trans_ids are temporary ids assigned to all files involved in a transform.
1581 It's possible, even common, that not all files in the Tree have trans_ids.
1582
1583 trans_ids are used because filenames and file_ids are not good enough
1584 identifiers; filenames change, and not all files have file_ids. File-ids
1585 are also associated with trans-ids, so that moving a file moves its
1586 file-id.
1587
1588 trans_ids are only valid for the TreeTransform that generated them.
1589
1590 Limbo
1591 -----
1592 Limbo is a temporary directory use to hold new versions of files.
1593 Files are added to limbo by create_file, create_directory, create_symlink,
1594 and their convenience variants (new_*). Files may be removed from limbo
1595 using cancel_creation. Files are renamed from limbo into their final
1596 location as part of TreeTransform.apply
1597
1598 Limbo must be cleaned up, by either calling TreeTransform.apply or
1599 calling TreeTransform.finalize.
1600
1601 Files are placed into limbo inside their parent directories, where
1602 possible. This reduces subsequent renames, and makes operations involving
1603 lots of files faster. This optimization is only possible if the parent
1604 directory is created *before* creating any of its children, so avoid
1605 creating children before parents, where possible.
1606
1607 Pending-deletion
1608 ----------------
1609 This temporary directory is used by _FileMover for storing files that are
1610 about to be deleted. In case of rollback, the files will be restored.
1611 FileMover does not delete files until it is sure that a rollback will not
1612 happen.
1613 """
1614
1615 def __init__(self, tree, pb=None):
1616 """Note: a tree_write lock is taken on the tree.
1617
1618 Use TreeTransform.finalize() to release the lock (can be omitted if
1619 TreeTransform.apply() called).
1620 """
1621 tree.lock_tree_write()
1622 try:
1623 limbodir = urlutils.local_path_from_url(
1624 tree._transport.abspath('limbo'))
1625 osutils.ensure_empty_directory_exists(
1626 limbodir,
1627 errors.ExistingLimbo)
1628 deletiondir = urlutils.local_path_from_url(
1629 tree._transport.abspath('pending-deletion'))
1630 osutils.ensure_empty_directory_exists(
1631 deletiondir,
1632 errors.ExistingPendingDeletion)
1633 except BaseException:
1634 tree.unlock()
1635 raise
1636
1637 # Cache of realpath results, to speed up canonical_path
1638 self._realpaths = {}
1639 # Cache of relpath results, to speed up canonical_path
1640 self._relpaths = {}
1641 DiskTreeTransform.__init__(self, tree, limbodir, pb,
1642 tree.case_sensitive)
1643 self._deletiondir = deletiondir
1644
1645 def canonical_path(self, path):
1646 """Get the canonical tree-relative path"""
1647 # don't follow final symlinks
1648 abs = self._tree.abspath(path)
1649 if abs in self._relpaths:
1650 return self._relpaths[abs]
1651 dirname, basename = os.path.split(abs)
1652 if dirname not in self._realpaths:
1653 self._realpaths[dirname] = os.path.realpath(dirname)
1654 dirname = self._realpaths[dirname]
1655 abs = pathjoin(dirname, basename)
1656 if dirname in self._relpaths:
1657 relpath = pathjoin(self._relpaths[dirname], basename)
1658 relpath = relpath.rstrip('/\\')
1659 else:
1660 relpath = self._tree.relpath(abs)
1661 self._relpaths[abs] = relpath
1662 return relpath
1663
1664 def tree_kind(self, trans_id):
1665 """Determine the file kind in the working tree.
1666
1667 :returns: The file kind or None if the file does not exist
1668 """
1669 path = self._tree_id_paths.get(trans_id)
1670 if path is None:
1671 return None
1672 try:
1673 return file_kind(self._tree.abspath(path))
1674 except errors.NoSuchFile:
1675 return None
1676
1677 def _set_mode(self, trans_id, mode_id, typefunc):
1678 """Set the mode of new file contents.
1679 The mode_id is the existing file to get the mode from (often the same
1680 as trans_id). The operation is only performed if there's a mode match
1681 according to typefunc.
1682 """
1683 if mode_id is None:
1684 mode_id = trans_id
1685 try:
1686 old_path = self._tree_id_paths[mode_id]
1687 except KeyError:
1688 return
1689 try:
1690 mode = os.stat(self._tree.abspath(old_path)).st_mode
1691 except OSError as e:
1692 if e.errno in (errno.ENOENT, errno.ENOTDIR):
1693 # Either old_path doesn't exist, or the parent of the
1694 # target is not a directory (but will be one eventually)
1695 # Either way, we know it doesn't exist *right now*
1696 # See also bug #248448
1697 return
1698 else:
1699 raise
1700 if typefunc(mode):
1701 osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1702
1703 def iter_tree_children(self, parent_id):
1704 """Iterate through the entry's tree children, if any"""
1705 try:
1706 path = self._tree_id_paths[parent_id]
1707 except KeyError:
1708 return
1709 try:
1710 children = os.listdir(self._tree.abspath(path))
1711 except OSError as e:
1712 if not (osutils._is_error_enotdir(e) or
1713 e.errno in (errno.ENOENT, errno.ESRCH)):
1714 raise
1715 return
1716
1717 for child in children:
1718 childpath = joinpath(path, child)
1719 if self._tree.is_control_filename(childpath):
1720 continue
1721 yield self.trans_id_tree_path(childpath)
1722
1723 def _generate_limbo_path(self, trans_id):
1724 """Generate a limbo path using the final path if possible.
1725
1726 This optimizes the performance of applying the tree transform by
1727 avoiding renames. These renames can be avoided only when the parent
1728 directory is already scheduled for creation.
1729
1730 If the final path cannot be used, falls back to using the trans_id as
1731 the relpath.
1732 """
1733 parent = self._new_parent.get(trans_id)
1734 # if the parent directory is already in limbo (e.g. when building a
1735 # tree), choose a limbo name inside the parent, to reduce further
1736 # renames.
1737 use_direct_path = False
1738 if self._new_contents.get(parent) == 'directory':
1739 filename = self._new_name.get(trans_id)
1740 if filename is not None:
1741 if parent not in self._limbo_children:
1742 self._limbo_children[parent] = set()
1743 self._limbo_children_names[parent] = {}
1744 use_direct_path = True
1745 # the direct path can only be used if no other file has
1746 # already taken this pathname, i.e. if the name is unused, or
1747 # if it is already associated with this trans_id.
1748 elif self._case_sensitive_target:
1749 if (self._limbo_children_names[parent].get(filename)
1750 in (trans_id, None)):
1751 use_direct_path = True
1752 else:
1753 for l_filename, l_trans_id in viewitems(
1754 self._limbo_children_names[parent]):
1755 if l_trans_id == trans_id:
1756 continue
1757 if l_filename.lower() == filename.lower():
1758 break
1759 else:
1760 use_direct_path = True
1761
1762 if not use_direct_path:
1763 return DiskTreeTransform._generate_limbo_path(self, trans_id)
1764
1765 limbo_name = pathjoin(self._limbo_files[parent], filename)
1766 self._limbo_children[parent].add(trans_id)
1767 self._limbo_children_names[parent][filename] = trans_id
1768 return limbo_name
1769
1770 def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1771 """Apply all changes to the inventory and filesystem.
1772
1773 If filesystem or inventory conflicts are present, MalformedTransform
1774 will be thrown.
1775
1776 If apply succeeds, finalize is not necessary.
1777
1778 :param no_conflicts: if True, the caller guarantees there are no
1779 conflicts, so no check is made.
1780 :param precomputed_delta: An inventory delta to use instead of
1781 calculating one.
1782 :param _mover: Supply an alternate FileMover, for testing
1783 """
1784 raise NotImplementedError(self.apply)
1785
1786
1787def joinpath(parent, child):1791def joinpath(parent, child):
1788 """Join tree-relative paths, handling the tree root specially"""1792 """Join tree-relative paths, handling the tree root specially"""
1789 if parent is None or parent == "":1793 if parent is None or parent == "":

Subscribers

People subscribed via source and target branches