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