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

Proposed by Jelmer Vernooij on 2020-07-05
Status: Merged
Approved by: Jelmer Vernooij on 2020-07-05
Approved revision: 7584
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz/transform2
Merge into: lp:brz/3.1
Diff against target: 1446 lines (+852/-413)
4 files modified
breezy/bzr/transform.py (+222/-3)
breezy/git/transform.py (+222/-4)
breezy/tests/per_workingtree/test_transform.py (+4/-6)
breezy/transform.py (+404/-400)
To merge this branch: bzr merge lp:~jelmer/brz/transform2
Reviewer Review Type Date Requested Status
Jelmer Vernooij Approve on 2020-07-05
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.
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 == "":

Subscribers

People subscribed via source and target branches