Merge lp:~jelmer/brz/transform2 into lp:brz/3.1
- transform2
- Merge into 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 |
Related bugs: |
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
1 | === modified file 'breezy/bzr/transform.py' | |||
2 | --- breezy/bzr/transform.py 2020-07-05 13:18:03 +0000 | |||
3 | +++ breezy/bzr/transform.py 2020-07-05 19:03:19 +0000 | |||
4 | @@ -29,6 +29,7 @@ | |||
5 | 29 | revision as _mod_revision, | 29 | revision as _mod_revision, |
6 | 30 | tree, | 30 | tree, |
7 | 31 | ui, | 31 | ui, |
8 | 32 | urlutils, | ||
9 | 32 | ) | 33 | ) |
10 | 33 | 34 | ||
11 | 34 | from ..i18n import gettext | 35 | from ..i18n import gettext |
12 | @@ -36,7 +37,6 @@ | |||
13 | 36 | from ..sixish import text_type, viewvalues, viewitems | 37 | from ..sixish import text_type, viewvalues, viewitems |
14 | 37 | from ..transform import ( | 38 | from ..transform import ( |
15 | 38 | ROOT_PARENT, | 39 | ROOT_PARENT, |
16 | 39 | TreeTransform, | ||
17 | 40 | _FileMover, | 40 | _FileMover, |
18 | 41 | _TransformResults, | 41 | _TransformResults, |
19 | 42 | DiskTreeTransform, | 42 | DiskTreeTransform, |
20 | @@ -52,8 +52,226 @@ | |||
21 | 52 | ) | 52 | ) |
22 | 53 | 53 | ||
23 | 54 | 54 | ||
26 | 55 | class InventoryTreeTransform(TreeTransform): | 55 | class InventoryTreeTransform(DiskTreeTransform): |
27 | 56 | """Tree transform for Bazaar trees.""" | 56 | """Represent a tree transformation. |
28 | 57 | |||
29 | 58 | This object is designed to support incremental generation of the transform, | ||
30 | 59 | in any order. | ||
31 | 60 | |||
32 | 61 | However, it gives optimum performance when parent directories are created | ||
33 | 62 | before their contents. The transform is then able to put child files | ||
34 | 63 | directly in their parent directory, avoiding later renames. | ||
35 | 64 | |||
36 | 65 | It is easy to produce malformed transforms, but they are generally | ||
37 | 66 | harmless. Attempting to apply a malformed transform will cause an | ||
38 | 67 | exception to be raised before any modifications are made to the tree. | ||
39 | 68 | |||
40 | 69 | Many kinds of malformed transforms can be corrected with the | ||
41 | 70 | resolve_conflicts function. The remaining ones indicate programming error, | ||
42 | 71 | such as trying to create a file with no path. | ||
43 | 72 | |||
44 | 73 | Two sets of file creation methods are supplied. Convenience methods are: | ||
45 | 74 | * new_file | ||
46 | 75 | * new_directory | ||
47 | 76 | * new_symlink | ||
48 | 77 | |||
49 | 78 | These are composed of the low-level methods: | ||
50 | 79 | * create_path | ||
51 | 80 | * create_file or create_directory or create_symlink | ||
52 | 81 | * version_file | ||
53 | 82 | * set_executability | ||
54 | 83 | |||
55 | 84 | Transform/Transaction ids | ||
56 | 85 | ------------------------- | ||
57 | 86 | trans_ids are temporary ids assigned to all files involved in a transform. | ||
58 | 87 | It's possible, even common, that not all files in the Tree have trans_ids. | ||
59 | 88 | |||
60 | 89 | trans_ids are used because filenames and file_ids are not good enough | ||
61 | 90 | identifiers; filenames change, and not all files have file_ids. File-ids | ||
62 | 91 | are also associated with trans-ids, so that moving a file moves its | ||
63 | 92 | file-id. | ||
64 | 93 | |||
65 | 94 | trans_ids are only valid for the TreeTransform that generated them. | ||
66 | 95 | |||
67 | 96 | Limbo | ||
68 | 97 | ----- | ||
69 | 98 | Limbo is a temporary directory use to hold new versions of files. | ||
70 | 99 | Files are added to limbo by create_file, create_directory, create_symlink, | ||
71 | 100 | and their convenience variants (new_*). Files may be removed from limbo | ||
72 | 101 | using cancel_creation. Files are renamed from limbo into their final | ||
73 | 102 | location as part of TreeTransform.apply | ||
74 | 103 | |||
75 | 104 | Limbo must be cleaned up, by either calling TreeTransform.apply or | ||
76 | 105 | calling TreeTransform.finalize. | ||
77 | 106 | |||
78 | 107 | Files are placed into limbo inside their parent directories, where | ||
79 | 108 | possible. This reduces subsequent renames, and makes operations involving | ||
80 | 109 | lots of files faster. This optimization is only possible if the parent | ||
81 | 110 | directory is created *before* creating any of its children, so avoid | ||
82 | 111 | creating children before parents, where possible. | ||
83 | 112 | |||
84 | 113 | Pending-deletion | ||
85 | 114 | ---------------- | ||
86 | 115 | This temporary directory is used by _FileMover for storing files that are | ||
87 | 116 | about to be deleted. In case of rollback, the files will be restored. | ||
88 | 117 | FileMover does not delete files until it is sure that a rollback will not | ||
89 | 118 | happen. | ||
90 | 119 | """ | ||
91 | 120 | |||
92 | 121 | def __init__(self, tree, pb=None): | ||
93 | 122 | """Note: a tree_write lock is taken on the tree. | ||
94 | 123 | |||
95 | 124 | Use TreeTransform.finalize() to release the lock (can be omitted if | ||
96 | 125 | TreeTransform.apply() called). | ||
97 | 126 | """ | ||
98 | 127 | tree.lock_tree_write() | ||
99 | 128 | try: | ||
100 | 129 | limbodir = urlutils.local_path_from_url( | ||
101 | 130 | tree._transport.abspath('limbo')) | ||
102 | 131 | osutils.ensure_empty_directory_exists( | ||
103 | 132 | limbodir, | ||
104 | 133 | errors.ExistingLimbo) | ||
105 | 134 | deletiondir = urlutils.local_path_from_url( | ||
106 | 135 | tree._transport.abspath('pending-deletion')) | ||
107 | 136 | osutils.ensure_empty_directory_exists( | ||
108 | 137 | deletiondir, | ||
109 | 138 | errors.ExistingPendingDeletion) | ||
110 | 139 | except BaseException: | ||
111 | 140 | tree.unlock() | ||
112 | 141 | raise | ||
113 | 142 | |||
114 | 143 | # Cache of realpath results, to speed up canonical_path | ||
115 | 144 | self._realpaths = {} | ||
116 | 145 | # Cache of relpath results, to speed up canonical_path | ||
117 | 146 | self._relpaths = {} | ||
118 | 147 | DiskTreeTransform.__init__(self, tree, limbodir, pb, | ||
119 | 148 | tree.case_sensitive) | ||
120 | 149 | self._deletiondir = deletiondir | ||
121 | 150 | |||
122 | 151 | def canonical_path(self, path): | ||
123 | 152 | """Get the canonical tree-relative path""" | ||
124 | 153 | # don't follow final symlinks | ||
125 | 154 | abs = self._tree.abspath(path) | ||
126 | 155 | if abs in self._relpaths: | ||
127 | 156 | return self._relpaths[abs] | ||
128 | 157 | dirname, basename = os.path.split(abs) | ||
129 | 158 | if dirname not in self._realpaths: | ||
130 | 159 | self._realpaths[dirname] = os.path.realpath(dirname) | ||
131 | 160 | dirname = self._realpaths[dirname] | ||
132 | 161 | abs = osutils.pathjoin(dirname, basename) | ||
133 | 162 | if dirname in self._relpaths: | ||
134 | 163 | relpath = osutils.pathjoin(self._relpaths[dirname], basename) | ||
135 | 164 | relpath = relpath.rstrip('/\\') | ||
136 | 165 | else: | ||
137 | 166 | relpath = self._tree.relpath(abs) | ||
138 | 167 | self._relpaths[abs] = relpath | ||
139 | 168 | return relpath | ||
140 | 169 | |||
141 | 170 | def tree_kind(self, trans_id): | ||
142 | 171 | """Determine the file kind in the working tree. | ||
143 | 172 | |||
144 | 173 | :returns: The file kind or None if the file does not exist | ||
145 | 174 | """ | ||
146 | 175 | path = self._tree_id_paths.get(trans_id) | ||
147 | 176 | if path is None: | ||
148 | 177 | return None | ||
149 | 178 | try: | ||
150 | 179 | return osutils.file_kind(self._tree.abspath(path)) | ||
151 | 180 | except errors.NoSuchFile: | ||
152 | 181 | return None | ||
153 | 182 | |||
154 | 183 | def _set_mode(self, trans_id, mode_id, typefunc): | ||
155 | 184 | """Set the mode of new file contents. | ||
156 | 185 | The mode_id is the existing file to get the mode from (often the same | ||
157 | 186 | as trans_id). The operation is only performed if there's a mode match | ||
158 | 187 | according to typefunc. | ||
159 | 188 | """ | ||
160 | 189 | if mode_id is None: | ||
161 | 190 | mode_id = trans_id | ||
162 | 191 | try: | ||
163 | 192 | old_path = self._tree_id_paths[mode_id] | ||
164 | 193 | except KeyError: | ||
165 | 194 | return | ||
166 | 195 | try: | ||
167 | 196 | mode = os.stat(self._tree.abspath(old_path)).st_mode | ||
168 | 197 | except OSError as e: | ||
169 | 198 | if e.errno in (errno.ENOENT, errno.ENOTDIR): | ||
170 | 199 | # Either old_path doesn't exist, or the parent of the | ||
171 | 200 | # target is not a directory (but will be one eventually) | ||
172 | 201 | # Either way, we know it doesn't exist *right now* | ||
173 | 202 | # See also bug #248448 | ||
174 | 203 | return | ||
175 | 204 | else: | ||
176 | 205 | raise | ||
177 | 206 | if typefunc(mode): | ||
178 | 207 | osutils.chmod_if_possible(self._limbo_name(trans_id), mode) | ||
179 | 208 | |||
180 | 209 | def iter_tree_children(self, parent_id): | ||
181 | 210 | """Iterate through the entry's tree children, if any""" | ||
182 | 211 | try: | ||
183 | 212 | path = self._tree_id_paths[parent_id] | ||
184 | 213 | except KeyError: | ||
185 | 214 | return | ||
186 | 215 | try: | ||
187 | 216 | children = os.listdir(self._tree.abspath(path)) | ||
188 | 217 | except OSError as e: | ||
189 | 218 | if not (osutils._is_error_enotdir(e) or | ||
190 | 219 | e.errno in (errno.ENOENT, errno.ESRCH)): | ||
191 | 220 | raise | ||
192 | 221 | return | ||
193 | 222 | |||
194 | 223 | for child in children: | ||
195 | 224 | childpath = joinpath(path, child) | ||
196 | 225 | if self._tree.is_control_filename(childpath): | ||
197 | 226 | continue | ||
198 | 227 | yield self.trans_id_tree_path(childpath) | ||
199 | 228 | |||
200 | 229 | def _generate_limbo_path(self, trans_id): | ||
201 | 230 | """Generate a limbo path using the final path if possible. | ||
202 | 231 | |||
203 | 232 | This optimizes the performance of applying the tree transform by | ||
204 | 233 | avoiding renames. These renames can be avoided only when the parent | ||
205 | 234 | directory is already scheduled for creation. | ||
206 | 235 | |||
207 | 236 | If the final path cannot be used, falls back to using the trans_id as | ||
208 | 237 | the relpath. | ||
209 | 238 | """ | ||
210 | 239 | parent = self._new_parent.get(trans_id) | ||
211 | 240 | # if the parent directory is already in limbo (e.g. when building a | ||
212 | 241 | # tree), choose a limbo name inside the parent, to reduce further | ||
213 | 242 | # renames. | ||
214 | 243 | use_direct_path = False | ||
215 | 244 | if self._new_contents.get(parent) == 'directory': | ||
216 | 245 | filename = self._new_name.get(trans_id) | ||
217 | 246 | if filename is not None: | ||
218 | 247 | if parent not in self._limbo_children: | ||
219 | 248 | self._limbo_children[parent] = set() | ||
220 | 249 | self._limbo_children_names[parent] = {} | ||
221 | 250 | use_direct_path = True | ||
222 | 251 | # the direct path can only be used if no other file has | ||
223 | 252 | # already taken this pathname, i.e. if the name is unused, or | ||
224 | 253 | # if it is already associated with this trans_id. | ||
225 | 254 | elif self._case_sensitive_target: | ||
226 | 255 | if (self._limbo_children_names[parent].get(filename) | ||
227 | 256 | in (trans_id, None)): | ||
228 | 257 | use_direct_path = True | ||
229 | 258 | else: | ||
230 | 259 | for l_filename, l_trans_id in viewitems( | ||
231 | 260 | self._limbo_children_names[parent]): | ||
232 | 261 | if l_trans_id == trans_id: | ||
233 | 262 | continue | ||
234 | 263 | if l_filename.lower() == filename.lower(): | ||
235 | 264 | break | ||
236 | 265 | else: | ||
237 | 266 | use_direct_path = True | ||
238 | 267 | |||
239 | 268 | if not use_direct_path: | ||
240 | 269 | return DiskTreeTransform._generate_limbo_path(self, trans_id) | ||
241 | 270 | |||
242 | 271 | limbo_name = osutils.pathjoin(self._limbo_files[parent], filename) | ||
243 | 272 | self._limbo_children[parent].add(trans_id) | ||
244 | 273 | self._limbo_children_names[parent][filename] = trans_id | ||
245 | 274 | return limbo_name | ||
246 | 57 | 275 | ||
247 | 58 | def version_file(self, trans_id, file_id=None): | 276 | def version_file(self, trans_id, file_id=None): |
248 | 59 | """Schedule a file to become versioned.""" | 277 | """Schedule a file to become versioned.""" |
249 | @@ -109,6 +327,7 @@ | |||
250 | 109 | hook(self._tree, self) | 327 | hook(self._tree, self) |
251 | 110 | if not no_conflicts: | 328 | if not no_conflicts: |
252 | 111 | self._check_malformed() | 329 | self._check_malformed() |
253 | 330 | self.rename_count = 0 | ||
254 | 112 | with ui.ui_factory.nested_progress_bar() as child_pb: | 331 | with ui.ui_factory.nested_progress_bar() as child_pb: |
255 | 113 | if precomputed_delta is None: | 332 | if precomputed_delta is None: |
256 | 114 | child_pb.update(gettext('Apply phase'), 0, 2) | 333 | child_pb.update(gettext('Apply phase'), 0, 2) |
257 | 115 | 334 | ||
258 | === modified file 'breezy/git/transform.py' | |||
259 | --- breezy/git/transform.py 2020-07-05 13:18:03 +0000 | |||
260 | +++ breezy/git/transform.py 2020-07-05 19:03:19 +0000 | |||
261 | @@ -20,15 +20,16 @@ | |||
262 | 20 | import errno | 20 | import errno |
263 | 21 | import os | 21 | import os |
264 | 22 | 22 | ||
266 | 23 | from .. import errors, ui | 23 | from .. import errors, osutils, ui, urlutils |
267 | 24 | from ..i18n import gettext | 24 | from ..i18n import gettext |
268 | 25 | from ..mutabletree import MutableTree | 25 | from ..mutabletree import MutableTree |
269 | 26 | from ..sixish import viewitems | 26 | from ..sixish import viewitems |
270 | 27 | from ..transform import ( | 27 | from ..transform import ( |
272 | 28 | TreeTransform, | 28 | DiskTreeTransform, |
273 | 29 | _TransformResults, | 29 | _TransformResults, |
274 | 30 | _FileMover, | 30 | _FileMover, |
275 | 31 | FinalPaths, | 31 | FinalPaths, |
276 | 32 | joinpath, | ||
277 | 32 | unique_add, | 33 | unique_add, |
278 | 33 | TransformRenameFailed, | 34 | TransformRenameFailed, |
279 | 34 | ) | 35 | ) |
280 | @@ -37,8 +38,224 @@ | |||
281 | 37 | from ..bzr.transform import TransformPreview as GitTransformPreview | 38 | from ..bzr.transform import TransformPreview as GitTransformPreview |
282 | 38 | 39 | ||
283 | 39 | 40 | ||
286 | 40 | class GitTreeTransform(TreeTransform): | 41 | class GitTreeTransform(DiskTreeTransform): |
287 | 41 | """Tree transform for Bazaar trees.""" | 42 | """Represent a tree transformation. |
288 | 43 | |||
289 | 44 | This object is designed to support incremental generation of the transform, | ||
290 | 45 | in any order. | ||
291 | 46 | |||
292 | 47 | However, it gives optimum performance when parent directories are created | ||
293 | 48 | before their contents. The transform is then able to put child files | ||
294 | 49 | directly in their parent directory, avoiding later renames. | ||
295 | 50 | |||
296 | 51 | It is easy to produce malformed transforms, but they are generally | ||
297 | 52 | harmless. Attempting to apply a malformed transform will cause an | ||
298 | 53 | exception to be raised before any modifications are made to the tree. | ||
299 | 54 | |||
300 | 55 | Many kinds of malformed transforms can be corrected with the | ||
301 | 56 | resolve_conflicts function. The remaining ones indicate programming error, | ||
302 | 57 | such as trying to create a file with no path. | ||
303 | 58 | |||
304 | 59 | Two sets of file creation methods are supplied. Convenience methods are: | ||
305 | 60 | * new_file | ||
306 | 61 | * new_directory | ||
307 | 62 | * new_symlink | ||
308 | 63 | |||
309 | 64 | These are composed of the low-level methods: | ||
310 | 65 | * create_path | ||
311 | 66 | * create_file or create_directory or create_symlink | ||
312 | 67 | * version_file | ||
313 | 68 | * set_executability | ||
314 | 69 | |||
315 | 70 | Transform/Transaction ids | ||
316 | 71 | ------------------------- | ||
317 | 72 | trans_ids are temporary ids assigned to all files involved in a transform. | ||
318 | 73 | It's possible, even common, that not all files in the Tree have trans_ids. | ||
319 | 74 | |||
320 | 75 | trans_ids are used because filenames and file_ids are not good enough | ||
321 | 76 | identifiers; filenames change. | ||
322 | 77 | |||
323 | 78 | trans_ids are only valid for the TreeTransform that generated them. | ||
324 | 79 | |||
325 | 80 | Limbo | ||
326 | 81 | ----- | ||
327 | 82 | Limbo is a temporary directory use to hold new versions of files. | ||
328 | 83 | Files are added to limbo by create_file, create_directory, create_symlink, | ||
329 | 84 | and their convenience variants (new_*). Files may be removed from limbo | ||
330 | 85 | using cancel_creation. Files are renamed from limbo into their final | ||
331 | 86 | location as part of TreeTransform.apply | ||
332 | 87 | |||
333 | 88 | Limbo must be cleaned up, by either calling TreeTransform.apply or | ||
334 | 89 | calling TreeTransform.finalize. | ||
335 | 90 | |||
336 | 91 | Files are placed into limbo inside their parent directories, where | ||
337 | 92 | possible. This reduces subsequent renames, and makes operations involving | ||
338 | 93 | lots of files faster. This optimization is only possible if the parent | ||
339 | 94 | directory is created *before* creating any of its children, so avoid | ||
340 | 95 | creating children before parents, where possible. | ||
341 | 96 | |||
342 | 97 | Pending-deletion | ||
343 | 98 | ---------------- | ||
344 | 99 | This temporary directory is used by _FileMover for storing files that are | ||
345 | 100 | about to be deleted. In case of rollback, the files will be restored. | ||
346 | 101 | FileMover does not delete files until it is sure that a rollback will not | ||
347 | 102 | happen. | ||
348 | 103 | """ | ||
349 | 104 | |||
350 | 105 | def __init__(self, tree, pb=None): | ||
351 | 106 | """Note: a tree_write lock is taken on the tree. | ||
352 | 107 | |||
353 | 108 | Use TreeTransform.finalize() to release the lock (can be omitted if | ||
354 | 109 | TreeTransform.apply() called). | ||
355 | 110 | """ | ||
356 | 111 | tree.lock_tree_write() | ||
357 | 112 | try: | ||
358 | 113 | limbodir = urlutils.local_path_from_url( | ||
359 | 114 | tree._transport.abspath('limbo')) | ||
360 | 115 | osutils.ensure_empty_directory_exists( | ||
361 | 116 | limbodir, | ||
362 | 117 | errors.ExistingLimbo) | ||
363 | 118 | deletiondir = urlutils.local_path_from_url( | ||
364 | 119 | tree._transport.abspath('pending-deletion')) | ||
365 | 120 | osutils.ensure_empty_directory_exists( | ||
366 | 121 | deletiondir, | ||
367 | 122 | errors.ExistingPendingDeletion) | ||
368 | 123 | except BaseException: | ||
369 | 124 | tree.unlock() | ||
370 | 125 | raise | ||
371 | 126 | |||
372 | 127 | # Cache of realpath results, to speed up canonical_path | ||
373 | 128 | self._realpaths = {} | ||
374 | 129 | # Cache of relpath results, to speed up canonical_path | ||
375 | 130 | self._relpaths = {} | ||
376 | 131 | DiskTreeTransform.__init__(self, tree, limbodir, pb, | ||
377 | 132 | tree.case_sensitive) | ||
378 | 133 | self._deletiondir = deletiondir | ||
379 | 134 | |||
380 | 135 | def canonical_path(self, path): | ||
381 | 136 | """Get the canonical tree-relative path""" | ||
382 | 137 | # don't follow final symlinks | ||
383 | 138 | abs = self._tree.abspath(path) | ||
384 | 139 | if abs in self._relpaths: | ||
385 | 140 | return self._relpaths[abs] | ||
386 | 141 | dirname, basename = os.path.split(abs) | ||
387 | 142 | if dirname not in self._realpaths: | ||
388 | 143 | self._realpaths[dirname] = os.path.realpath(dirname) | ||
389 | 144 | dirname = self._realpaths[dirname] | ||
390 | 145 | abs = osutils.pathjoin(dirname, basename) | ||
391 | 146 | if dirname in self._relpaths: | ||
392 | 147 | relpath = osutils.pathjoin(self._relpaths[dirname], basename) | ||
393 | 148 | relpath = relpath.rstrip('/\\') | ||
394 | 149 | else: | ||
395 | 150 | relpath = self._tree.relpath(abs) | ||
396 | 151 | self._relpaths[abs] = relpath | ||
397 | 152 | return relpath | ||
398 | 153 | |||
399 | 154 | def tree_kind(self, trans_id): | ||
400 | 155 | """Determine the file kind in the working tree. | ||
401 | 156 | |||
402 | 157 | :returns: The file kind or None if the file does not exist | ||
403 | 158 | """ | ||
404 | 159 | path = self._tree_id_paths.get(trans_id) | ||
405 | 160 | if path is None: | ||
406 | 161 | return None | ||
407 | 162 | try: | ||
408 | 163 | return osutils.file_kind(self._tree.abspath(path)) | ||
409 | 164 | except errors.NoSuchFile: | ||
410 | 165 | return None | ||
411 | 166 | |||
412 | 167 | def _set_mode(self, trans_id, mode_id, typefunc): | ||
413 | 168 | """Set the mode of new file contents. | ||
414 | 169 | The mode_id is the existing file to get the mode from (often the same | ||
415 | 170 | as trans_id). The operation is only performed if there's a mode match | ||
416 | 171 | according to typefunc. | ||
417 | 172 | """ | ||
418 | 173 | if mode_id is None: | ||
419 | 174 | mode_id = trans_id | ||
420 | 175 | try: | ||
421 | 176 | old_path = self._tree_id_paths[mode_id] | ||
422 | 177 | except KeyError: | ||
423 | 178 | return | ||
424 | 179 | try: | ||
425 | 180 | mode = os.stat(self._tree.abspath(old_path)).st_mode | ||
426 | 181 | except OSError as e: | ||
427 | 182 | if e.errno in (errno.ENOENT, errno.ENOTDIR): | ||
428 | 183 | # Either old_path doesn't exist, or the parent of the | ||
429 | 184 | # target is not a directory (but will be one eventually) | ||
430 | 185 | # Either way, we know it doesn't exist *right now* | ||
431 | 186 | # See also bug #248448 | ||
432 | 187 | return | ||
433 | 188 | else: | ||
434 | 189 | raise | ||
435 | 190 | if typefunc(mode): | ||
436 | 191 | osutils.chmod_if_possible(self._limbo_name(trans_id), mode) | ||
437 | 192 | |||
438 | 193 | def iter_tree_children(self, parent_id): | ||
439 | 194 | """Iterate through the entry's tree children, if any""" | ||
440 | 195 | try: | ||
441 | 196 | path = self._tree_id_paths[parent_id] | ||
442 | 197 | except KeyError: | ||
443 | 198 | return | ||
444 | 199 | try: | ||
445 | 200 | children = os.listdir(self._tree.abspath(path)) | ||
446 | 201 | except OSError as e: | ||
447 | 202 | if not (osutils._is_error_enotdir(e) or | ||
448 | 203 | e.errno in (errno.ENOENT, errno.ESRCH)): | ||
449 | 204 | raise | ||
450 | 205 | return | ||
451 | 206 | |||
452 | 207 | for child in children: | ||
453 | 208 | childpath = joinpath(path, child) | ||
454 | 209 | if self._tree.is_control_filename(childpath): | ||
455 | 210 | continue | ||
456 | 211 | yield self.trans_id_tree_path(childpath) | ||
457 | 212 | |||
458 | 213 | def _generate_limbo_path(self, trans_id): | ||
459 | 214 | """Generate a limbo path using the final path if possible. | ||
460 | 215 | |||
461 | 216 | This optimizes the performance of applying the tree transform by | ||
462 | 217 | avoiding renames. These renames can be avoided only when the parent | ||
463 | 218 | directory is already scheduled for creation. | ||
464 | 219 | |||
465 | 220 | If the final path cannot be used, falls back to using the trans_id as | ||
466 | 221 | the relpath. | ||
467 | 222 | """ | ||
468 | 223 | parent = self._new_parent.get(trans_id) | ||
469 | 224 | # if the parent directory is already in limbo (e.g. when building a | ||
470 | 225 | # tree), choose a limbo name inside the parent, to reduce further | ||
471 | 226 | # renames. | ||
472 | 227 | use_direct_path = False | ||
473 | 228 | if self._new_contents.get(parent) == 'directory': | ||
474 | 229 | filename = self._new_name.get(trans_id) | ||
475 | 230 | if filename is not None: | ||
476 | 231 | if parent not in self._limbo_children: | ||
477 | 232 | self._limbo_children[parent] = set() | ||
478 | 233 | self._limbo_children_names[parent] = {} | ||
479 | 234 | use_direct_path = True | ||
480 | 235 | # the direct path can only be used if no other file has | ||
481 | 236 | # already taken this pathname, i.e. if the name is unused, or | ||
482 | 237 | # if it is already associated with this trans_id. | ||
483 | 238 | elif self._case_sensitive_target: | ||
484 | 239 | if (self._limbo_children_names[parent].get(filename) | ||
485 | 240 | in (trans_id, None)): | ||
486 | 241 | use_direct_path = True | ||
487 | 242 | else: | ||
488 | 243 | for l_filename, l_trans_id in viewitems( | ||
489 | 244 | self._limbo_children_names[parent]): | ||
490 | 245 | if l_trans_id == trans_id: | ||
491 | 246 | continue | ||
492 | 247 | if l_filename.lower() == filename.lower(): | ||
493 | 248 | break | ||
494 | 249 | else: | ||
495 | 250 | use_direct_path = True | ||
496 | 251 | |||
497 | 252 | if not use_direct_path: | ||
498 | 253 | return DiskTreeTransform._generate_limbo_path(self, trans_id) | ||
499 | 254 | |||
500 | 255 | limbo_name = osutils.pathjoin(self._limbo_files[parent], filename) | ||
501 | 256 | self._limbo_children[parent].add(trans_id) | ||
502 | 257 | self._limbo_children_names[parent][filename] = trans_id | ||
503 | 258 | return limbo_name | ||
504 | 42 | 259 | ||
505 | 43 | def version_file(self, trans_id, file_id=None): | 260 | def version_file(self, trans_id, file_id=None): |
506 | 44 | """Schedule a file to become versioned.""" | 261 | """Schedule a file to become versioned.""" |
507 | @@ -71,6 +288,7 @@ | |||
508 | 71 | hook(self._tree, self) | 288 | hook(self._tree, self) |
509 | 72 | if not no_conflicts: | 289 | if not no_conflicts: |
510 | 73 | self._check_malformed() | 290 | self._check_malformed() |
511 | 291 | self.rename_count = 0 | ||
512 | 74 | with ui.ui_factory.nested_progress_bar() as child_pb: | 292 | with ui.ui_factory.nested_progress_bar() as child_pb: |
513 | 75 | if precomputed_delta is None: | 293 | if precomputed_delta is None: |
514 | 76 | child_pb.update(gettext('Apply phase'), 0, 2) | 294 | child_pb.update(gettext('Apply phase'), 0, 2) |
515 | 77 | 295 | ||
516 | === modified file 'breezy/tests/per_workingtree/test_transform.py' | |||
517 | --- breezy/tests/per_workingtree/test_transform.py 2020-07-05 13:18:03 +0000 | |||
518 | +++ breezy/tests/per_workingtree/test_transform.py 2020-07-05 19:03:19 +0000 | |||
519 | @@ -1326,15 +1326,13 @@ | |||
520 | 1326 | def test_rename_count(self): | 1326 | def test_rename_count(self): |
521 | 1327 | transform, root = self.transform() | 1327 | transform, root = self.transform() |
522 | 1328 | transform.new_file('name1', root, [b'contents']) | 1328 | transform.new_file('name1', root, [b'contents']) |
526 | 1329 | self.assertEqual(transform.rename_count, 0) | 1329 | result = transform.apply() |
527 | 1330 | transform.apply() | 1330 | self.assertEqual(result.rename_count, 1) |
525 | 1331 | self.assertEqual(transform.rename_count, 1) | ||
528 | 1332 | transform2, root = self.transform() | 1331 | transform2, root = self.transform() |
529 | 1333 | transform2.adjust_path('name2', root, | 1332 | transform2.adjust_path('name2', root, |
530 | 1334 | transform2.trans_id_tree_path('name1')) | 1333 | transform2.trans_id_tree_path('name1')) |
534 | 1335 | self.assertEqual(transform2.rename_count, 0) | 1334 | result = transform2.apply() |
535 | 1336 | transform2.apply() | 1335 | self.assertEqual(result.rename_count, 2) |
533 | 1337 | self.assertEqual(transform2.rename_count, 2) | ||
536 | 1338 | 1336 | ||
537 | 1339 | def test_change_parent(self): | 1337 | def test_change_parent(self): |
538 | 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. |
539 | 1341 | 1339 | ||
540 | === modified file 'breezy/transform.py' | |||
541 | --- breezy/transform.py 2020-07-05 13:18:03 +0000 | |||
542 | +++ breezy/transform.py 2020-07-05 19:03:19 +0000 | |||
543 | @@ -126,66 +126,75 @@ | |||
544 | 126 | 126 | ||
545 | 127 | 127 | ||
546 | 128 | class _TransformResults(object): | 128 | class _TransformResults(object): |
547 | 129 | |||
548 | 129 | def __init__(self, modified_paths, rename_count): | 130 | def __init__(self, modified_paths, rename_count): |
549 | 130 | object.__init__(self) | 131 | object.__init__(self) |
550 | 131 | self.modified_paths = modified_paths | 132 | self.modified_paths = modified_paths |
551 | 132 | self.rename_count = rename_count | 133 | self.rename_count = rename_count |
552 | 133 | 134 | ||
553 | 134 | 135 | ||
567 | 135 | class TreeTransformBase(object): | 136 | class TreeTransform(object): |
568 | 136 | """The base class for TreeTransform and its kin.""" | 137 | """Represent a tree transformation. |
569 | 137 | 138 | ||
570 | 138 | def __init__(self, tree, pb=None, case_sensitive=True): | 139 | This object is designed to support incremental generation of the transform, |
571 | 139 | """Constructor. | 140 | in any order. |
572 | 140 | 141 | ||
573 | 141 | :param tree: The tree that will be transformed, but not necessarily | 142 | However, it gives optimum performance when parent directories are created |
574 | 142 | the output tree. | 143 | before their contents. The transform is then able to put child files |
575 | 143 | :param pb: ignored | 144 | directly in their parent directory, avoiding later renames. |
576 | 144 | :param case_sensitive: If True, the target of the transform is | 145 | |
577 | 145 | case sensitive, not just case preserving. | 146 | It is easy to produce malformed transforms, but they are generally |
578 | 146 | """ | 147 | harmless. Attempting to apply a malformed transform will cause an |
579 | 147 | object.__init__(self) | 148 | exception to be raised before any modifications are made to the tree. |
580 | 149 | |||
581 | 150 | Many kinds of malformed transforms can be corrected with the | ||
582 | 151 | resolve_conflicts function. The remaining ones indicate programming error, | ||
583 | 152 | such as trying to create a file with no path. | ||
584 | 153 | |||
585 | 154 | Two sets of file creation methods are supplied. Convenience methods are: | ||
586 | 155 | * new_file | ||
587 | 156 | * new_directory | ||
588 | 157 | * new_symlink | ||
589 | 158 | |||
590 | 159 | These are composed of the low-level methods: | ||
591 | 160 | * create_path | ||
592 | 161 | * create_file or create_directory or create_symlink | ||
593 | 162 | * version_file | ||
594 | 163 | * set_executability | ||
595 | 164 | |||
596 | 165 | Transform/Transaction ids | ||
597 | 166 | ------------------------- | ||
598 | 167 | trans_ids are temporary ids assigned to all files involved in a transform. | ||
599 | 168 | It's possible, even common, that not all files in the Tree have trans_ids. | ||
600 | 169 | |||
601 | 170 | trans_ids are only valid for the TreeTransform that generated them. | ||
602 | 171 | """ | ||
603 | 172 | |||
604 | 173 | def __init__(self, tree, pb=None): | ||
605 | 148 | self._tree = tree | 174 | self._tree = tree |
606 | 175 | # A progress bar | ||
607 | 176 | self._pb = pb | ||
608 | 149 | self._id_number = 0 | 177 | self._id_number = 0 |
609 | 178 | # Mapping of path in old tree -> trans_id | ||
610 | 179 | self._tree_path_ids = {} | ||
611 | 180 | # Mapping trans_id -> path in old tree | ||
612 | 181 | self._tree_id_paths = {} | ||
613 | 150 | # mapping of trans_id -> new basename | 182 | # mapping of trans_id -> new basename |
614 | 151 | self._new_name = {} | 183 | self._new_name = {} |
615 | 152 | # mapping of trans_id -> new parent trans_id | 184 | # mapping of trans_id -> new parent trans_id |
616 | 153 | self._new_parent = {} | 185 | self._new_parent = {} |
617 | 154 | # mapping of trans_id with new contents -> new file_kind | 186 | # mapping of trans_id with new contents -> new file_kind |
618 | 155 | self._new_contents = {} | 187 | self._new_contents = {} |
619 | 156 | # mapping of trans_id => (sha1 of content, stat_value) | ||
620 | 157 | self._observed_sha1s = {} | ||
621 | 158 | # Set of trans_ids whose contents will be removed | 188 | # Set of trans_ids whose contents will be removed |
622 | 159 | self._removed_contents = set() | 189 | self._removed_contents = set() |
623 | 160 | # Mapping of trans_id -> new execute-bit value | 190 | # Mapping of trans_id -> new execute-bit value |
624 | 161 | self._new_executability = {} | 191 | self._new_executability = {} |
625 | 162 | # Mapping of trans_id -> new tree-reference value | 192 | # Mapping of trans_id -> new tree-reference value |
626 | 163 | self._new_reference_revision = {} | 193 | self._new_reference_revision = {} |
627 | 164 | # Mapping of trans_id -> new file_id | ||
628 | 165 | self._new_id = {} | ||
629 | 166 | # Mapping of old file-id -> trans_id | ||
630 | 167 | self._non_present_ids = {} | ||
631 | 168 | # Mapping of new file_id -> trans_id | ||
632 | 169 | self._r_new_id = {} | ||
633 | 170 | # Set of trans_ids that will be removed | 194 | # Set of trans_ids that will be removed |
634 | 171 | self._removed_id = set() | 195 | self._removed_id = set() |
635 | 172 | # Mapping of path in old tree -> trans_id | ||
636 | 173 | self._tree_path_ids = {} | ||
637 | 174 | # Mapping trans_id -> path in old tree | ||
638 | 175 | self._tree_id_paths = {} | ||
639 | 176 | # The trans_id that will be used as the tree root | ||
640 | 177 | if tree.is_versioned(''): | ||
641 | 178 | self._new_root = self.trans_id_tree_path('') | ||
642 | 179 | else: | ||
643 | 180 | self._new_root = None | ||
644 | 181 | # Indicator of whether the transform has been applied | 196 | # Indicator of whether the transform has been applied |
645 | 182 | self._done = False | 197 | self._done = False |
646 | 183 | # A progress bar | ||
647 | 184 | self._pb = pb | ||
648 | 185 | # Whether the target is case sensitive | ||
649 | 186 | self._case_sensitive_target = case_sensitive | ||
650 | 187 | # A counter of how many files have been renamed | ||
651 | 188 | self.rename_count = 0 | ||
652 | 189 | 198 | ||
653 | 190 | def __enter__(self): | 199 | def __enter__(self): |
654 | 191 | """Support Context Manager API.""" | 200 | """Support Context Manager API.""" |
655 | @@ -203,15 +212,354 @@ | |||
656 | 203 | """ | 212 | """ |
657 | 204 | raise NotImplementedError(self.iter_tree_children) | 213 | raise NotImplementedError(self.iter_tree_children) |
658 | 205 | 214 | ||
659 | 206 | def _read_symlink_target(self, trans_id): | ||
660 | 207 | raise NotImplementedError(self._read_symlink_target) | ||
661 | 208 | |||
662 | 209 | def canonical_path(self, path): | 215 | def canonical_path(self, path): |
663 | 210 | return path | 216 | return path |
664 | 211 | 217 | ||
665 | 212 | def tree_kind(self, trans_id): | 218 | def tree_kind(self, trans_id): |
666 | 213 | raise NotImplementedError(self.tree_kind) | 219 | raise NotImplementedError(self.tree_kind) |
667 | 214 | 220 | ||
668 | 221 | def by_parent(self): | ||
669 | 222 | """Return a map of parent: children for known parents. | ||
670 | 223 | |||
671 | 224 | Only new paths and parents of tree files with assigned ids are used. | ||
672 | 225 | """ | ||
673 | 226 | by_parent = {} | ||
674 | 227 | items = list(viewitems(self._new_parent)) | ||
675 | 228 | items.extend((t, self.final_parent(t)) | ||
676 | 229 | for t in list(self._tree_id_paths)) | ||
677 | 230 | for trans_id, parent_id in items: | ||
678 | 231 | if parent_id not in by_parent: | ||
679 | 232 | by_parent[parent_id] = set() | ||
680 | 233 | by_parent[parent_id].add(trans_id) | ||
681 | 234 | return by_parent | ||
682 | 235 | |||
683 | 236 | def finalize(self): | ||
684 | 237 | """Release the working tree lock, if held. | ||
685 | 238 | |||
686 | 239 | This is required if apply has not been invoked, but can be invoked | ||
687 | 240 | even after apply. | ||
688 | 241 | """ | ||
689 | 242 | raise NotImplementedError(self.finalize) | ||
690 | 243 | |||
691 | 244 | def create_path(self, name, parent): | ||
692 | 245 | """Assign a transaction id to a new path""" | ||
693 | 246 | trans_id = self._assign_id() | ||
694 | 247 | unique_add(self._new_name, trans_id, name) | ||
695 | 248 | unique_add(self._new_parent, trans_id, parent) | ||
696 | 249 | return trans_id | ||
697 | 250 | |||
698 | 251 | def adjust_path(self, name, parent, trans_id): | ||
699 | 252 | """Change the path that is assigned to a transaction id.""" | ||
700 | 253 | if parent is None: | ||
701 | 254 | raise ValueError("Parent trans-id may not be None") | ||
702 | 255 | if trans_id == self._new_root: | ||
703 | 256 | raise CantMoveRoot | ||
704 | 257 | self._new_name[trans_id] = name | ||
705 | 258 | self._new_parent[trans_id] = parent | ||
706 | 259 | |||
707 | 260 | def adjust_root_path(self, name, parent): | ||
708 | 261 | """Emulate moving the root by moving all children, instead. | ||
709 | 262 | |||
710 | 263 | We do this by undoing the association of root's transaction id with the | ||
711 | 264 | current tree. This allows us to create a new directory with that | ||
712 | 265 | transaction id. We unversion the root directory and version the | ||
713 | 266 | physically new directory, and hope someone versions the tree root | ||
714 | 267 | later. | ||
715 | 268 | """ | ||
716 | 269 | raise NotImplementedError(self.adjust_root_path) | ||
717 | 270 | |||
718 | 271 | def fixup_new_roots(self): | ||
719 | 272 | """Reinterpret requests to change the root directory | ||
720 | 273 | |||
721 | 274 | Instead of creating a root directory, or moving an existing directory, | ||
722 | 275 | all the attributes and children of the new root are applied to the | ||
723 | 276 | existing root directory. | ||
724 | 277 | |||
725 | 278 | This means that the old root trans-id becomes obsolete, so it is | ||
726 | 279 | recommended only to invoke this after the root trans-id has become | ||
727 | 280 | irrelevant. | ||
728 | 281 | """ | ||
729 | 282 | raise NotImplementedError(self.fixup_new_roots) | ||
730 | 283 | |||
731 | 284 | def _assign_id(self): | ||
732 | 285 | """Produce a new tranform id""" | ||
733 | 286 | new_id = "new-%s" % self._id_number | ||
734 | 287 | self._id_number += 1 | ||
735 | 288 | return new_id | ||
736 | 289 | |||
737 | 290 | def trans_id_tree_path(self, path): | ||
738 | 291 | """Determine (and maybe set) the transaction ID for a tree path.""" | ||
739 | 292 | path = self.canonical_path(path) | ||
740 | 293 | if path not in self._tree_path_ids: | ||
741 | 294 | self._tree_path_ids[path] = self._assign_id() | ||
742 | 295 | self._tree_id_paths[self._tree_path_ids[path]] = path | ||
743 | 296 | return self._tree_path_ids[path] | ||
744 | 297 | |||
745 | 298 | def get_tree_parent(self, trans_id): | ||
746 | 299 | """Determine id of the parent in the tree.""" | ||
747 | 300 | path = self._tree_id_paths[trans_id] | ||
748 | 301 | if path == "": | ||
749 | 302 | return ROOT_PARENT | ||
750 | 303 | return self.trans_id_tree_path(os.path.dirname(path)) | ||
751 | 304 | |||
752 | 305 | def delete_contents(self, trans_id): | ||
753 | 306 | """Schedule the contents of a path entry for deletion""" | ||
754 | 307 | kind = self.tree_kind(trans_id) | ||
755 | 308 | if kind is not None: | ||
756 | 309 | self._removed_contents.add(trans_id) | ||
757 | 310 | |||
758 | 311 | def cancel_deletion(self, trans_id): | ||
759 | 312 | """Cancel a scheduled deletion""" | ||
760 | 313 | self._removed_contents.remove(trans_id) | ||
761 | 314 | |||
762 | 315 | def delete_versioned(self, trans_id): | ||
763 | 316 | """Delete and unversion a versioned file""" | ||
764 | 317 | self.delete_contents(trans_id) | ||
765 | 318 | self.unversion_file(trans_id) | ||
766 | 319 | |||
767 | 320 | def set_executability(self, executability, trans_id): | ||
768 | 321 | """Schedule setting of the 'execute' bit | ||
769 | 322 | To unschedule, set to None | ||
770 | 323 | """ | ||
771 | 324 | if executability is None: | ||
772 | 325 | del self._new_executability[trans_id] | ||
773 | 326 | else: | ||
774 | 327 | unique_add(self._new_executability, trans_id, executability) | ||
775 | 328 | |||
776 | 329 | def set_tree_reference(self, revision_id, trans_id): | ||
777 | 330 | """Set the reference associated with a directory""" | ||
778 | 331 | unique_add(self._new_reference_revision, trans_id, revision_id) | ||
779 | 332 | |||
780 | 333 | def version_file(self, trans_id, file_id=None): | ||
781 | 334 | """Schedule a file to become versioned.""" | ||
782 | 335 | raise NotImplementedError(self.version_file) | ||
783 | 336 | |||
784 | 337 | def cancel_versioning(self, trans_id): | ||
785 | 338 | """Undo a previous versioning of a file""" | ||
786 | 339 | raise NotImplementedError(self.cancel_versioning) | ||
787 | 340 | |||
788 | 341 | def unversion_file(self, trans_id): | ||
789 | 342 | """Schedule a path entry to become unversioned""" | ||
790 | 343 | self._removed_id.add(trans_id) | ||
791 | 344 | |||
792 | 345 | def new_paths(self, filesystem_only=False): | ||
793 | 346 | """Determine the paths of all new and changed files. | ||
794 | 347 | |||
795 | 348 | :param filesystem_only: if True, only calculate values for files | ||
796 | 349 | that require renames or execute bit changes. | ||
797 | 350 | """ | ||
798 | 351 | raise NotImplementedError(self.new_paths) | ||
799 | 352 | |||
800 | 353 | def final_kind(self, trans_id): | ||
801 | 354 | """Determine the final file kind, after any changes applied. | ||
802 | 355 | |||
803 | 356 | :return: None if the file does not exist/has no contents. (It is | ||
804 | 357 | conceivable that a path would be created without the corresponding | ||
805 | 358 | contents insertion command) | ||
806 | 359 | """ | ||
807 | 360 | if trans_id in self._new_contents: | ||
808 | 361 | return self._new_contents[trans_id] | ||
809 | 362 | elif trans_id in self._removed_contents: | ||
810 | 363 | return None | ||
811 | 364 | else: | ||
812 | 365 | return self.tree_kind(trans_id) | ||
813 | 366 | |||
814 | 367 | def tree_path(self, trans_id): | ||
815 | 368 | """Determine the tree path associated with the trans_id.""" | ||
816 | 369 | return self._tree_id_paths.get(trans_id) | ||
817 | 370 | |||
818 | 371 | def final_is_versioned(self, trans_id): | ||
819 | 372 | raise NotImplementedError(self.final_is_versioned) | ||
820 | 373 | |||
821 | 374 | def final_parent(self, trans_id): | ||
822 | 375 | """Determine the parent file_id, after any changes are applied. | ||
823 | 376 | |||
824 | 377 | ROOT_PARENT is returned for the tree root. | ||
825 | 378 | """ | ||
826 | 379 | try: | ||
827 | 380 | return self._new_parent[trans_id] | ||
828 | 381 | except KeyError: | ||
829 | 382 | return self.get_tree_parent(trans_id) | ||
830 | 383 | |||
831 | 384 | def final_name(self, trans_id): | ||
832 | 385 | """Determine the final filename, after all changes are applied.""" | ||
833 | 386 | try: | ||
834 | 387 | return self._new_name[trans_id] | ||
835 | 388 | except KeyError: | ||
836 | 389 | try: | ||
837 | 390 | return os.path.basename(self._tree_id_paths[trans_id]) | ||
838 | 391 | except KeyError: | ||
839 | 392 | raise NoFinalPath(trans_id, self) | ||
840 | 393 | |||
841 | 394 | def path_changed(self, trans_id): | ||
842 | 395 | """Return True if a trans_id's path has changed.""" | ||
843 | 396 | return (trans_id in self._new_name) or (trans_id in self._new_parent) | ||
844 | 397 | |||
845 | 398 | def new_contents(self, trans_id): | ||
846 | 399 | return (trans_id in self._new_contents) | ||
847 | 400 | |||
848 | 401 | def find_conflicts(self): | ||
849 | 402 | """Find any violations of inventory or filesystem invariants""" | ||
850 | 403 | raise NotImplementedError(self.find_conflicts) | ||
851 | 404 | |||
852 | 405 | def new_file(self, name, parent_id, contents, file_id=None, | ||
853 | 406 | executable=None, sha1=None): | ||
854 | 407 | """Convenience method to create files. | ||
855 | 408 | |||
856 | 409 | name is the name of the file to create. | ||
857 | 410 | parent_id is the transaction id of the parent directory of the file. | ||
858 | 411 | contents is an iterator of bytestrings, which will be used to produce | ||
859 | 412 | the file. | ||
860 | 413 | :param file_id: The inventory ID of the file, if it is to be versioned. | ||
861 | 414 | :param executable: Only valid when a file_id has been supplied. | ||
862 | 415 | """ | ||
863 | 416 | raise NotImplementedError(self.new_file) | ||
864 | 417 | |||
865 | 418 | def new_directory(self, name, parent_id, file_id=None): | ||
866 | 419 | """Convenience method to create directories. | ||
867 | 420 | |||
868 | 421 | name is the name of the directory to create. | ||
869 | 422 | parent_id is the transaction id of the parent directory of the | ||
870 | 423 | directory. | ||
871 | 424 | file_id is the inventory ID of the directory, if it is to be versioned. | ||
872 | 425 | """ | ||
873 | 426 | raise NotImplementedError(self.new_directory) | ||
874 | 427 | |||
875 | 428 | def new_symlink(self, name, parent_id, target, file_id=None): | ||
876 | 429 | """Convenience method to create symbolic link. | ||
877 | 430 | |||
878 | 431 | name is the name of the symlink to create. | ||
879 | 432 | parent_id is the transaction id of the parent directory of the symlink. | ||
880 | 433 | target is a bytestring of the target of the symlink. | ||
881 | 434 | file_id is the inventory ID of the file, if it is to be versioned. | ||
882 | 435 | """ | ||
883 | 436 | raise NotImplementedError(self.new_symlink) | ||
884 | 437 | |||
885 | 438 | def new_orphan(self, trans_id, parent_id): | ||
886 | 439 | """Schedule an item to be orphaned. | ||
887 | 440 | |||
888 | 441 | When a directory is about to be removed, its children, if they are not | ||
889 | 442 | versioned are moved out of the way: they don't have a parent anymore. | ||
890 | 443 | |||
891 | 444 | :param trans_id: The trans_id of the existing item. | ||
892 | 445 | :param parent_id: The parent trans_id of the item. | ||
893 | 446 | """ | ||
894 | 447 | raise NotImplementedError(self.new_orphan) | ||
895 | 448 | |||
896 | 449 | def iter_changes(self): | ||
897 | 450 | """Produce output in the same format as Tree.iter_changes. | ||
898 | 451 | |||
899 | 452 | Will produce nonsensical results if invoked while inventory/filesystem | ||
900 | 453 | conflicts (as reported by TreeTransform.find_conflicts()) are present. | ||
901 | 454 | |||
902 | 455 | This reads the Transform, but only reproduces changes involving a | ||
903 | 456 | file_id. Files that are not versioned in either of the FROM or TO | ||
904 | 457 | states are not reflected. | ||
905 | 458 | """ | ||
906 | 459 | raise NotImplementedError(self.iter_changes) | ||
907 | 460 | |||
908 | 461 | def get_preview_tree(self): | ||
909 | 462 | """Return a tree representing the result of the transform. | ||
910 | 463 | |||
911 | 464 | The tree is a snapshot, and altering the TreeTransform will invalidate | ||
912 | 465 | it. | ||
913 | 466 | """ | ||
914 | 467 | raise NotImplementedError(self.get_preview_tree) | ||
915 | 468 | |||
916 | 469 | def commit(self, branch, message, merge_parents=None, strict=False, | ||
917 | 470 | timestamp=None, timezone=None, committer=None, authors=None, | ||
918 | 471 | revprops=None, revision_id=None): | ||
919 | 472 | """Commit the result of this TreeTransform to a branch. | ||
920 | 473 | |||
921 | 474 | :param branch: The branch to commit to. | ||
922 | 475 | :param message: The message to attach to the commit. | ||
923 | 476 | :param merge_parents: Additional parent revision-ids specified by | ||
924 | 477 | pending merges. | ||
925 | 478 | :param strict: If True, abort the commit if there are unversioned | ||
926 | 479 | files. | ||
927 | 480 | :param timestamp: if not None, seconds-since-epoch for the time and | ||
928 | 481 | date. (May be a float.) | ||
929 | 482 | :param timezone: Optional timezone for timestamp, as an offset in | ||
930 | 483 | seconds. | ||
931 | 484 | :param committer: Optional committer in email-id format. | ||
932 | 485 | (e.g. "J Random Hacker <jrandom@example.com>") | ||
933 | 486 | :param authors: Optional list of authors in email-id format. | ||
934 | 487 | :param revprops: Optional dictionary of revision properties. | ||
935 | 488 | :param revision_id: Optional revision id. (Specifying a revision-id | ||
936 | 489 | may reduce performance for some non-native formats.) | ||
937 | 490 | :return: The revision_id of the revision committed. | ||
938 | 491 | """ | ||
939 | 492 | raise NotImplementedError(self.commit) | ||
940 | 493 | |||
941 | 494 | def create_file(self, contents, trans_id, mode_id=None, sha1=None): | ||
942 | 495 | """Schedule creation of a new file. | ||
943 | 496 | |||
944 | 497 | :seealso: new_file. | ||
945 | 498 | |||
946 | 499 | :param contents: an iterator of strings, all of which will be written | ||
947 | 500 | to the target destination. | ||
948 | 501 | :param trans_id: TreeTransform handle | ||
949 | 502 | :param mode_id: If not None, force the mode of the target file to match | ||
950 | 503 | the mode of the object referenced by mode_id. | ||
951 | 504 | Otherwise, we will try to preserve mode bits of an existing file. | ||
952 | 505 | :param sha1: If the sha1 of this content is already known, pass it in. | ||
953 | 506 | We can use it to prevent future sha1 computations. | ||
954 | 507 | """ | ||
955 | 508 | raise NotImplementedError(self.create_file) | ||
956 | 509 | |||
957 | 510 | def create_directory(self, trans_id): | ||
958 | 511 | """Schedule creation of a new directory. | ||
959 | 512 | |||
960 | 513 | See also new_directory. | ||
961 | 514 | """ | ||
962 | 515 | raise NotImplementedError(self.create_directory) | ||
963 | 516 | |||
964 | 517 | def create_symlink(self, target, trans_id): | ||
965 | 518 | """Schedule creation of a new symbolic link. | ||
966 | 519 | |||
967 | 520 | target is a bytestring. | ||
968 | 521 | See also new_symlink. | ||
969 | 522 | """ | ||
970 | 523 | raise NotImplementedError(self.create_symlink) | ||
971 | 524 | |||
972 | 525 | def create_hardlink(self, path, trans_id): | ||
973 | 526 | """Schedule creation of a hard link""" | ||
974 | 527 | raise NotImplementedError(self.create_hardlink) | ||
975 | 528 | |||
976 | 529 | def cancel_creation(self, trans_id): | ||
977 | 530 | """Cancel the creation of new file contents.""" | ||
978 | 531 | raise NotImplementedError(self.cancel_creation) | ||
979 | 532 | |||
980 | 533 | |||
981 | 534 | class TreeTransformBase(TreeTransform): | ||
982 | 535 | """The base class for TreeTransform and its kin.""" | ||
983 | 536 | |||
984 | 537 | def __init__(self, tree, pb=None, case_sensitive=True): | ||
985 | 538 | """Constructor. | ||
986 | 539 | |||
987 | 540 | :param tree: The tree that will be transformed, but not necessarily | ||
988 | 541 | the output tree. | ||
989 | 542 | :param pb: ignored | ||
990 | 543 | :param case_sensitive: If True, the target of the transform is | ||
991 | 544 | case sensitive, not just case preserving. | ||
992 | 545 | """ | ||
993 | 546 | super(TreeTransformBase, self).__init__(tree, pb=pb) | ||
994 | 547 | # mapping of trans_id => (sha1 of content, stat_value) | ||
995 | 548 | self._observed_sha1s = {} | ||
996 | 549 | # Mapping of trans_id -> new file_id | ||
997 | 550 | self._new_id = {} | ||
998 | 551 | # Mapping of old file-id -> trans_id | ||
999 | 552 | self._non_present_ids = {} | ||
1000 | 553 | # Mapping of new file_id -> trans_id | ||
1001 | 554 | self._r_new_id = {} | ||
1002 | 555 | # The trans_id that will be used as the tree root | ||
1003 | 556 | if tree.is_versioned(''): | ||
1004 | 557 | self._new_root = self.trans_id_tree_path('') | ||
1005 | 558 | else: | ||
1006 | 559 | self._new_root = None | ||
1007 | 560 | # Whether the target is case sensitive | ||
1008 | 561 | self._case_sensitive_target = case_sensitive | ||
1009 | 562 | |||
1010 | 215 | def finalize(self): | 563 | def finalize(self): |
1011 | 216 | """Release the working tree lock, if held. | 564 | """Release the working tree lock, if held. |
1012 | 217 | 565 | ||
1013 | @@ -230,12 +578,6 @@ | |||
1014 | 230 | 578 | ||
1015 | 231 | root = property(__get_root) | 579 | root = property(__get_root) |
1016 | 232 | 580 | ||
1017 | 233 | def _assign_id(self): | ||
1018 | 234 | """Produce a new tranform id""" | ||
1019 | 235 | new_id = "new-%s" % self._id_number | ||
1020 | 236 | self._id_number += 1 | ||
1021 | 237 | return new_id | ||
1022 | 238 | |||
1023 | 239 | def create_path(self, name, parent): | 581 | def create_path(self, name, parent): |
1024 | 240 | """Assign a transaction id to a new path""" | 582 | """Assign a transaction id to a new path""" |
1025 | 241 | trans_id = self._assign_id() | 583 | trans_id = self._assign_id() |
1026 | @@ -243,15 +585,6 @@ | |||
1027 | 243 | unique_add(self._new_parent, trans_id, parent) | 585 | unique_add(self._new_parent, trans_id, parent) |
1028 | 244 | return trans_id | 586 | return trans_id |
1029 | 245 | 587 | ||
1030 | 246 | def adjust_path(self, name, parent, trans_id): | ||
1031 | 247 | """Change the path that is assigned to a transaction id.""" | ||
1032 | 248 | if parent is None: | ||
1033 | 249 | raise ValueError("Parent trans-id may not be None") | ||
1034 | 250 | if trans_id == self._new_root: | ||
1035 | 251 | raise CantMoveRoot | ||
1036 | 252 | self._new_name[trans_id] = name | ||
1037 | 253 | self._new_parent[trans_id] = parent | ||
1038 | 254 | |||
1039 | 255 | def adjust_root_path(self, name, parent): | 588 | def adjust_root_path(self, name, parent): |
1040 | 256 | """Emulate moving the root by moving all children, instead. | 589 | """Emulate moving the root by moving all children, instead. |
1041 | 257 | 590 | ||
1042 | @@ -368,53 +701,6 @@ | |||
1043 | 368 | else: | 701 | else: |
1044 | 369 | return self.trans_id_tree_path(path) | 702 | return self.trans_id_tree_path(path) |
1045 | 370 | 703 | ||
1046 | 371 | def trans_id_tree_path(self, path): | ||
1047 | 372 | """Determine (and maybe set) the transaction ID for a tree path.""" | ||
1048 | 373 | path = self.canonical_path(path) | ||
1049 | 374 | if path not in self._tree_path_ids: | ||
1050 | 375 | self._tree_path_ids[path] = self._assign_id() | ||
1051 | 376 | self._tree_id_paths[self._tree_path_ids[path]] = path | ||
1052 | 377 | return self._tree_path_ids[path] | ||
1053 | 378 | |||
1054 | 379 | def get_tree_parent(self, trans_id): | ||
1055 | 380 | """Determine id of the parent in the tree.""" | ||
1056 | 381 | path = self._tree_id_paths[trans_id] | ||
1057 | 382 | if path == "": | ||
1058 | 383 | return ROOT_PARENT | ||
1059 | 384 | return self.trans_id_tree_path(os.path.dirname(path)) | ||
1060 | 385 | |||
1061 | 386 | def delete_contents(self, trans_id): | ||
1062 | 387 | """Schedule the contents of a path entry for deletion""" | ||
1063 | 388 | kind = self.tree_kind(trans_id) | ||
1064 | 389 | if kind is not None: | ||
1065 | 390 | self._removed_contents.add(trans_id) | ||
1066 | 391 | |||
1067 | 392 | def cancel_deletion(self, trans_id): | ||
1068 | 393 | """Cancel a scheduled deletion""" | ||
1069 | 394 | self._removed_contents.remove(trans_id) | ||
1070 | 395 | |||
1071 | 396 | def unversion_file(self, trans_id): | ||
1072 | 397 | """Schedule a path entry to become unversioned""" | ||
1073 | 398 | self._removed_id.add(trans_id) | ||
1074 | 399 | |||
1075 | 400 | def delete_versioned(self, trans_id): | ||
1076 | 401 | """Delete and unversion a versioned file""" | ||
1077 | 402 | self.delete_contents(trans_id) | ||
1078 | 403 | self.unversion_file(trans_id) | ||
1079 | 404 | |||
1080 | 405 | def set_executability(self, executability, trans_id): | ||
1081 | 406 | """Schedule setting of the 'execute' bit | ||
1082 | 407 | To unschedule, set to None | ||
1083 | 408 | """ | ||
1084 | 409 | if executability is None: | ||
1085 | 410 | del self._new_executability[trans_id] | ||
1086 | 411 | else: | ||
1087 | 412 | unique_add(self._new_executability, trans_id, executability) | ||
1088 | 413 | |||
1089 | 414 | def set_tree_reference(self, revision_id, trans_id): | ||
1090 | 415 | """Set the reference associated with a directory""" | ||
1091 | 416 | unique_add(self._new_reference_revision, trans_id, revision_id) | ||
1092 | 417 | |||
1093 | 418 | def version_file(self, trans_id, file_id=None): | 704 | def version_file(self, trans_id, file_id=None): |
1094 | 419 | """Schedule a file to become versioned.""" | 705 | """Schedule a file to become versioned.""" |
1095 | 420 | raise NotImplementedError(self.version_file) | 706 | raise NotImplementedError(self.version_file) |
1096 | @@ -444,24 +730,6 @@ | |||
1097 | 444 | new_ids.update(id_set) | 730 | new_ids.update(id_set) |
1098 | 445 | return sorted(FinalPaths(self).get_paths(new_ids)) | 731 | return sorted(FinalPaths(self).get_paths(new_ids)) |
1099 | 446 | 732 | ||
1100 | 447 | def final_kind(self, trans_id): | ||
1101 | 448 | """Determine the final file kind, after any changes applied. | ||
1102 | 449 | |||
1103 | 450 | :return: None if the file does not exist/has no contents. (It is | ||
1104 | 451 | conceivable that a path would be created without the corresponding | ||
1105 | 452 | contents insertion command) | ||
1106 | 453 | """ | ||
1107 | 454 | if trans_id in self._new_contents: | ||
1108 | 455 | return self._new_contents[trans_id] | ||
1109 | 456 | elif trans_id in self._removed_contents: | ||
1110 | 457 | return None | ||
1111 | 458 | else: | ||
1112 | 459 | return self.tree_kind(trans_id) | ||
1113 | 460 | |||
1114 | 461 | def tree_path(self, trans_id): | ||
1115 | 462 | """Determine the tree path associated with the trans_id.""" | ||
1116 | 463 | return self._tree_id_paths.get(trans_id) | ||
1117 | 464 | |||
1118 | 465 | def tree_file_id(self, trans_id): | 733 | def tree_file_id(self, trans_id): |
1119 | 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""" |
1120 | 467 | path = self.tree_path(trans_id) | 735 | path = self.tree_path(trans_id) |
1121 | @@ -500,48 +768,6 @@ | |||
1122 | 500 | if value == trans_id: | 768 | if value == trans_id: |
1123 | 501 | return key | 769 | return key |
1124 | 502 | 770 | ||
1125 | 503 | def final_parent(self, trans_id): | ||
1126 | 504 | """Determine the parent file_id, after any changes are applied. | ||
1127 | 505 | |||
1128 | 506 | ROOT_PARENT is returned for the tree root. | ||
1129 | 507 | """ | ||
1130 | 508 | try: | ||
1131 | 509 | return self._new_parent[trans_id] | ||
1132 | 510 | except KeyError: | ||
1133 | 511 | return self.get_tree_parent(trans_id) | ||
1134 | 512 | |||
1135 | 513 | def final_name(self, trans_id): | ||
1136 | 514 | """Determine the final filename, after all changes are applied.""" | ||
1137 | 515 | try: | ||
1138 | 516 | return self._new_name[trans_id] | ||
1139 | 517 | except KeyError: | ||
1140 | 518 | try: | ||
1141 | 519 | return os.path.basename(self._tree_id_paths[trans_id]) | ||
1142 | 520 | except KeyError: | ||
1143 | 521 | raise NoFinalPath(trans_id, self) | ||
1144 | 522 | |||
1145 | 523 | def by_parent(self): | ||
1146 | 524 | """Return a map of parent: children for known parents. | ||
1147 | 525 | |||
1148 | 526 | Only new paths and parents of tree files with assigned ids are used. | ||
1149 | 527 | """ | ||
1150 | 528 | by_parent = {} | ||
1151 | 529 | items = list(viewitems(self._new_parent)) | ||
1152 | 530 | items.extend((t, self.final_parent(t)) | ||
1153 | 531 | for t in list(self._tree_id_paths)) | ||
1154 | 532 | for trans_id, parent_id in items: | ||
1155 | 533 | if parent_id not in by_parent: | ||
1156 | 534 | by_parent[parent_id] = set() | ||
1157 | 535 | by_parent[parent_id].add(trans_id) | ||
1158 | 536 | return by_parent | ||
1159 | 537 | |||
1160 | 538 | def path_changed(self, trans_id): | ||
1161 | 539 | """Return True if a trans_id's path has changed.""" | ||
1162 | 540 | return (trans_id in self._new_name) or (trans_id in self._new_parent) | ||
1163 | 541 | |||
1164 | 542 | def new_contents(self, trans_id): | ||
1165 | 543 | return (trans_id in self._new_contents) | ||
1166 | 544 | |||
1167 | 545 | def find_conflicts(self): | 771 | def find_conflicts(self): |
1168 | 546 | """Find any violations of inventory or filesystem invariants""" | 772 | """Find any violations of inventory or filesystem invariants""" |
1169 | 547 | if self._done is True: | 773 | if self._done is True: |
1170 | @@ -1240,6 +1466,22 @@ | |||
1171 | 1240 | """Cancel the creation of new file contents.""" | 1466 | """Cancel the creation of new file contents.""" |
1172 | 1241 | raise NotImplementedError(self.cancel_creation) | 1467 | raise NotImplementedError(self.cancel_creation) |
1173 | 1242 | 1468 | ||
1174 | 1469 | def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None): | ||
1175 | 1470 | """Apply all changes to the inventory and filesystem. | ||
1176 | 1471 | |||
1177 | 1472 | If filesystem or inventory conflicts are present, MalformedTransform | ||
1178 | 1473 | will be thrown. | ||
1179 | 1474 | |||
1180 | 1475 | If apply succeeds, finalize is not necessary. | ||
1181 | 1476 | |||
1182 | 1477 | :param no_conflicts: if True, the caller guarantees there are no | ||
1183 | 1478 | conflicts, so no check is made. | ||
1184 | 1479 | :param precomputed_delta: An inventory delta to use instead of | ||
1185 | 1480 | calculating one. | ||
1186 | 1481 | :param _mover: Supply an alternate FileMover, for testing | ||
1187 | 1482 | """ | ||
1188 | 1483 | raise NotImplementedError(self.apply) | ||
1189 | 1484 | |||
1190 | 1243 | 1485 | ||
1191 | 1244 | class DiskTreeTransform(TreeTransformBase): | 1486 | class DiskTreeTransform(TreeTransformBase): |
1192 | 1245 | """Tree transform storing its contents on disk.""" | 1487 | """Tree transform storing its contents on disk.""" |
1193 | @@ -1328,7 +1570,7 @@ | |||
1194 | 1328 | def adjust_path(self, name, parent, trans_id): | 1570 | def adjust_path(self, name, parent, trans_id): |
1195 | 1329 | previous_parent = self._new_parent.get(trans_id) | 1571 | previous_parent = self._new_parent.get(trans_id) |
1196 | 1330 | previous_name = self._new_name.get(trans_id) | 1572 | previous_name = self._new_name.get(trans_id) |
1198 | 1331 | TreeTransformBase.adjust_path(self, name, parent, trans_id) | 1573 | super(DiskTreeTransform, self).adjust_path(name, parent, trans_id) |
1199 | 1332 | if (trans_id in self._limbo_files | 1574 | if (trans_id in self._limbo_files |
1200 | 1333 | and trans_id not in self._needs_rename): | 1575 | and trans_id not in self._needs_rename): |
1201 | 1334 | self._rename_in_limbo([trans_id]) | 1576 | self._rename_in_limbo([trans_id]) |
1202 | @@ -1546,244 +1788,6 @@ | |||
1203 | 1546 | invalid='warning') | 1788 | invalid='warning') |
1204 | 1547 | 1789 | ||
1205 | 1548 | 1790 | ||
1206 | 1549 | class TreeTransform(DiskTreeTransform): | ||
1207 | 1550 | """Represent a tree transformation. | ||
1208 | 1551 | |||
1209 | 1552 | This object is designed to support incremental generation of the transform, | ||
1210 | 1553 | in any order. | ||
1211 | 1554 | |||
1212 | 1555 | However, it gives optimum performance when parent directories are created | ||
1213 | 1556 | before their contents. The transform is then able to put child files | ||
1214 | 1557 | directly in their parent directory, avoiding later renames. | ||
1215 | 1558 | |||
1216 | 1559 | It is easy to produce malformed transforms, but they are generally | ||
1217 | 1560 | harmless. Attempting to apply a malformed transform will cause an | ||
1218 | 1561 | exception to be raised before any modifications are made to the tree. | ||
1219 | 1562 | |||
1220 | 1563 | Many kinds of malformed transforms can be corrected with the | ||
1221 | 1564 | resolve_conflicts function. The remaining ones indicate programming error, | ||
1222 | 1565 | such as trying to create a file with no path. | ||
1223 | 1566 | |||
1224 | 1567 | Two sets of file creation methods are supplied. Convenience methods are: | ||
1225 | 1568 | * new_file | ||
1226 | 1569 | * new_directory | ||
1227 | 1570 | * new_symlink | ||
1228 | 1571 | |||
1229 | 1572 | These are composed of the low-level methods: | ||
1230 | 1573 | * create_path | ||
1231 | 1574 | * create_file or create_directory or create_symlink | ||
1232 | 1575 | * version_file | ||
1233 | 1576 | * set_executability | ||
1234 | 1577 | |||
1235 | 1578 | Transform/Transaction ids | ||
1236 | 1579 | ------------------------- | ||
1237 | 1580 | trans_ids are temporary ids assigned to all files involved in a transform. | ||
1238 | 1581 | It's possible, even common, that not all files in the Tree have trans_ids. | ||
1239 | 1582 | |||
1240 | 1583 | trans_ids are used because filenames and file_ids are not good enough | ||
1241 | 1584 | identifiers; filenames change, and not all files have file_ids. File-ids | ||
1242 | 1585 | are also associated with trans-ids, so that moving a file moves its | ||
1243 | 1586 | file-id. | ||
1244 | 1587 | |||
1245 | 1588 | trans_ids are only valid for the TreeTransform that generated them. | ||
1246 | 1589 | |||
1247 | 1590 | Limbo | ||
1248 | 1591 | ----- | ||
1249 | 1592 | Limbo is a temporary directory use to hold new versions of files. | ||
1250 | 1593 | Files are added to limbo by create_file, create_directory, create_symlink, | ||
1251 | 1594 | and their convenience variants (new_*). Files may be removed from limbo | ||
1252 | 1595 | using cancel_creation. Files are renamed from limbo into their final | ||
1253 | 1596 | location as part of TreeTransform.apply | ||
1254 | 1597 | |||
1255 | 1598 | Limbo must be cleaned up, by either calling TreeTransform.apply or | ||
1256 | 1599 | calling TreeTransform.finalize. | ||
1257 | 1600 | |||
1258 | 1601 | Files are placed into limbo inside their parent directories, where | ||
1259 | 1602 | possible. This reduces subsequent renames, and makes operations involving | ||
1260 | 1603 | lots of files faster. This optimization is only possible if the parent | ||
1261 | 1604 | directory is created *before* creating any of its children, so avoid | ||
1262 | 1605 | creating children before parents, where possible. | ||
1263 | 1606 | |||
1264 | 1607 | Pending-deletion | ||
1265 | 1608 | ---------------- | ||
1266 | 1609 | This temporary directory is used by _FileMover for storing files that are | ||
1267 | 1610 | about to be deleted. In case of rollback, the files will be restored. | ||
1268 | 1611 | FileMover does not delete files until it is sure that a rollback will not | ||
1269 | 1612 | happen. | ||
1270 | 1613 | """ | ||
1271 | 1614 | |||
1272 | 1615 | def __init__(self, tree, pb=None): | ||
1273 | 1616 | """Note: a tree_write lock is taken on the tree. | ||
1274 | 1617 | |||
1275 | 1618 | Use TreeTransform.finalize() to release the lock (can be omitted if | ||
1276 | 1619 | TreeTransform.apply() called). | ||
1277 | 1620 | """ | ||
1278 | 1621 | tree.lock_tree_write() | ||
1279 | 1622 | try: | ||
1280 | 1623 | limbodir = urlutils.local_path_from_url( | ||
1281 | 1624 | tree._transport.abspath('limbo')) | ||
1282 | 1625 | osutils.ensure_empty_directory_exists( | ||
1283 | 1626 | limbodir, | ||
1284 | 1627 | errors.ExistingLimbo) | ||
1285 | 1628 | deletiondir = urlutils.local_path_from_url( | ||
1286 | 1629 | tree._transport.abspath('pending-deletion')) | ||
1287 | 1630 | osutils.ensure_empty_directory_exists( | ||
1288 | 1631 | deletiondir, | ||
1289 | 1632 | errors.ExistingPendingDeletion) | ||
1290 | 1633 | except BaseException: | ||
1291 | 1634 | tree.unlock() | ||
1292 | 1635 | raise | ||
1293 | 1636 | |||
1294 | 1637 | # Cache of realpath results, to speed up canonical_path | ||
1295 | 1638 | self._realpaths = {} | ||
1296 | 1639 | # Cache of relpath results, to speed up canonical_path | ||
1297 | 1640 | self._relpaths = {} | ||
1298 | 1641 | DiskTreeTransform.__init__(self, tree, limbodir, pb, | ||
1299 | 1642 | tree.case_sensitive) | ||
1300 | 1643 | self._deletiondir = deletiondir | ||
1301 | 1644 | |||
1302 | 1645 | def canonical_path(self, path): | ||
1303 | 1646 | """Get the canonical tree-relative path""" | ||
1304 | 1647 | # don't follow final symlinks | ||
1305 | 1648 | abs = self._tree.abspath(path) | ||
1306 | 1649 | if abs in self._relpaths: | ||
1307 | 1650 | return self._relpaths[abs] | ||
1308 | 1651 | dirname, basename = os.path.split(abs) | ||
1309 | 1652 | if dirname not in self._realpaths: | ||
1310 | 1653 | self._realpaths[dirname] = os.path.realpath(dirname) | ||
1311 | 1654 | dirname = self._realpaths[dirname] | ||
1312 | 1655 | abs = pathjoin(dirname, basename) | ||
1313 | 1656 | if dirname in self._relpaths: | ||
1314 | 1657 | relpath = pathjoin(self._relpaths[dirname], basename) | ||
1315 | 1658 | relpath = relpath.rstrip('/\\') | ||
1316 | 1659 | else: | ||
1317 | 1660 | relpath = self._tree.relpath(abs) | ||
1318 | 1661 | self._relpaths[abs] = relpath | ||
1319 | 1662 | return relpath | ||
1320 | 1663 | |||
1321 | 1664 | def tree_kind(self, trans_id): | ||
1322 | 1665 | """Determine the file kind in the working tree. | ||
1323 | 1666 | |||
1324 | 1667 | :returns: The file kind or None if the file does not exist | ||
1325 | 1668 | """ | ||
1326 | 1669 | path = self._tree_id_paths.get(trans_id) | ||
1327 | 1670 | if path is None: | ||
1328 | 1671 | return None | ||
1329 | 1672 | try: | ||
1330 | 1673 | return file_kind(self._tree.abspath(path)) | ||
1331 | 1674 | except errors.NoSuchFile: | ||
1332 | 1675 | return None | ||
1333 | 1676 | |||
1334 | 1677 | def _set_mode(self, trans_id, mode_id, typefunc): | ||
1335 | 1678 | """Set the mode of new file contents. | ||
1336 | 1679 | The mode_id is the existing file to get the mode from (often the same | ||
1337 | 1680 | as trans_id). The operation is only performed if there's a mode match | ||
1338 | 1681 | according to typefunc. | ||
1339 | 1682 | """ | ||
1340 | 1683 | if mode_id is None: | ||
1341 | 1684 | mode_id = trans_id | ||
1342 | 1685 | try: | ||
1343 | 1686 | old_path = self._tree_id_paths[mode_id] | ||
1344 | 1687 | except KeyError: | ||
1345 | 1688 | return | ||
1346 | 1689 | try: | ||
1347 | 1690 | mode = os.stat(self._tree.abspath(old_path)).st_mode | ||
1348 | 1691 | except OSError as e: | ||
1349 | 1692 | if e.errno in (errno.ENOENT, errno.ENOTDIR): | ||
1350 | 1693 | # Either old_path doesn't exist, or the parent of the | ||
1351 | 1694 | # target is not a directory (but will be one eventually) | ||
1352 | 1695 | # Either way, we know it doesn't exist *right now* | ||
1353 | 1696 | # See also bug #248448 | ||
1354 | 1697 | return | ||
1355 | 1698 | else: | ||
1356 | 1699 | raise | ||
1357 | 1700 | if typefunc(mode): | ||
1358 | 1701 | osutils.chmod_if_possible(self._limbo_name(trans_id), mode) | ||
1359 | 1702 | |||
1360 | 1703 | def iter_tree_children(self, parent_id): | ||
1361 | 1704 | """Iterate through the entry's tree children, if any""" | ||
1362 | 1705 | try: | ||
1363 | 1706 | path = self._tree_id_paths[parent_id] | ||
1364 | 1707 | except KeyError: | ||
1365 | 1708 | return | ||
1366 | 1709 | try: | ||
1367 | 1710 | children = os.listdir(self._tree.abspath(path)) | ||
1368 | 1711 | except OSError as e: | ||
1369 | 1712 | if not (osutils._is_error_enotdir(e) or | ||
1370 | 1713 | e.errno in (errno.ENOENT, errno.ESRCH)): | ||
1371 | 1714 | raise | ||
1372 | 1715 | return | ||
1373 | 1716 | |||
1374 | 1717 | for child in children: | ||
1375 | 1718 | childpath = joinpath(path, child) | ||
1376 | 1719 | if self._tree.is_control_filename(childpath): | ||
1377 | 1720 | continue | ||
1378 | 1721 | yield self.trans_id_tree_path(childpath) | ||
1379 | 1722 | |||
1380 | 1723 | def _generate_limbo_path(self, trans_id): | ||
1381 | 1724 | """Generate a limbo path using the final path if possible. | ||
1382 | 1725 | |||
1383 | 1726 | This optimizes the performance of applying the tree transform by | ||
1384 | 1727 | avoiding renames. These renames can be avoided only when the parent | ||
1385 | 1728 | directory is already scheduled for creation. | ||
1386 | 1729 | |||
1387 | 1730 | If the final path cannot be used, falls back to using the trans_id as | ||
1388 | 1731 | the relpath. | ||
1389 | 1732 | """ | ||
1390 | 1733 | parent = self._new_parent.get(trans_id) | ||
1391 | 1734 | # if the parent directory is already in limbo (e.g. when building a | ||
1392 | 1735 | # tree), choose a limbo name inside the parent, to reduce further | ||
1393 | 1736 | # renames. | ||
1394 | 1737 | use_direct_path = False | ||
1395 | 1738 | if self._new_contents.get(parent) == 'directory': | ||
1396 | 1739 | filename = self._new_name.get(trans_id) | ||
1397 | 1740 | if filename is not None: | ||
1398 | 1741 | if parent not in self._limbo_children: | ||
1399 | 1742 | self._limbo_children[parent] = set() | ||
1400 | 1743 | self._limbo_children_names[parent] = {} | ||
1401 | 1744 | use_direct_path = True | ||
1402 | 1745 | # the direct path can only be used if no other file has | ||
1403 | 1746 | # already taken this pathname, i.e. if the name is unused, or | ||
1404 | 1747 | # if it is already associated with this trans_id. | ||
1405 | 1748 | elif self._case_sensitive_target: | ||
1406 | 1749 | if (self._limbo_children_names[parent].get(filename) | ||
1407 | 1750 | in (trans_id, None)): | ||
1408 | 1751 | use_direct_path = True | ||
1409 | 1752 | else: | ||
1410 | 1753 | for l_filename, l_trans_id in viewitems( | ||
1411 | 1754 | self._limbo_children_names[parent]): | ||
1412 | 1755 | if l_trans_id == trans_id: | ||
1413 | 1756 | continue | ||
1414 | 1757 | if l_filename.lower() == filename.lower(): | ||
1415 | 1758 | break | ||
1416 | 1759 | else: | ||
1417 | 1760 | use_direct_path = True | ||
1418 | 1761 | |||
1419 | 1762 | if not use_direct_path: | ||
1420 | 1763 | return DiskTreeTransform._generate_limbo_path(self, trans_id) | ||
1421 | 1764 | |||
1422 | 1765 | limbo_name = pathjoin(self._limbo_files[parent], filename) | ||
1423 | 1766 | self._limbo_children[parent].add(trans_id) | ||
1424 | 1767 | self._limbo_children_names[parent][filename] = trans_id | ||
1425 | 1768 | return limbo_name | ||
1426 | 1769 | |||
1427 | 1770 | def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None): | ||
1428 | 1771 | """Apply all changes to the inventory and filesystem. | ||
1429 | 1772 | |||
1430 | 1773 | If filesystem or inventory conflicts are present, MalformedTransform | ||
1431 | 1774 | will be thrown. | ||
1432 | 1775 | |||
1433 | 1776 | If apply succeeds, finalize is not necessary. | ||
1434 | 1777 | |||
1435 | 1778 | :param no_conflicts: if True, the caller guarantees there are no | ||
1436 | 1779 | conflicts, so no check is made. | ||
1437 | 1780 | :param precomputed_delta: An inventory delta to use instead of | ||
1438 | 1781 | calculating one. | ||
1439 | 1782 | :param _mover: Supply an alternate FileMover, for testing | ||
1440 | 1783 | """ | ||
1441 | 1784 | raise NotImplementedError(self.apply) | ||
1442 | 1785 | |||
1443 | 1786 | |||
1444 | 1787 | def joinpath(parent, child): | 1791 | def joinpath(parent, child): |
1445 | 1788 | """Join tree-relative paths, handling the tree root specially""" | 1792 | """Join tree-relative paths, handling the tree root specially""" |
1446 | 1789 | if parent is None or parent == "": | 1793 | if parent is None or parent == "": |