1# Copyright (C) 2006-2011 Canonical Ltd
2# Copyright (C) 2020 Breezy Developers
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18from __future__ import absolute_import
19
20import contextlib
21import errno
22import os
23from stat import S_IEXEC, S_ISREG
24import time
25
26from .. import (
27    annotate,
28    conflicts,
29    controldir,
30    errors,
31    lock,
32    multiparent,
33    osutils,
34    revision as _mod_revision,
35    trace,
36    tree,
37    ui,
38    urlutils,
39    )
40
41from ..filters import filtered_output_bytes, ContentFilterContext
42from ..i18n import gettext
43from ..mutabletree import MutableTree
44from ..progress import ProgressPhase
45from ..transform import (
46    ROOT_PARENT,
47    _FileMover,
48    _TransformResults,
49    joinpath,
50    NoFinalPath,
51    FinalPaths,
52    unique_add,
53    TreeTransform,
54    TransformRenameFailed,
55    ImmortalLimbo,
56    ReusingTransform,
57    MalformedTransform,
58    PreviewTree,
59    new_by_entry,
60    _reparent_children,
61    resolve_conflicts,
62    )
63from ..tree import find_previous_path
64from .conflicts import Conflict
65
66from . import (
67    inventory,
68    inventorytree,
69    )
70
71
72def _content_match(tree, entry, tree_path, kind, target_path):
73    if entry.kind != kind:
74        return False
75    if entry.kind == "directory":
76        return True
77    if entry.kind == "file":
78        with open(target_path, 'rb') as f1, \
79                tree.get_file(tree_path) as f2:
80            if osutils.compare_files(f1, f2):
81                return True
82    elif entry.kind == "symlink":
83        if tree.get_symlink_target(tree_path) == os.readlink(target_path):
84            return True
85    return False
86
87
88class TreeTransformBase(TreeTransform):
89    """The base class for TreeTransform and its kin."""
90
91    def __init__(self, tree, pb=None, case_sensitive=True):
92        """Constructor.
93
94        :param tree: The tree that will be transformed, but not necessarily
95            the output tree.
96        :param pb: ignored
97        :param case_sensitive: If True, the target of the transform is
98            case sensitive, not just case preserving.
99        """
100        super(TreeTransformBase, self).__init__(tree, pb=pb)
101        # mapping of trans_id => (sha1 of content, stat_value)
102        self._observed_sha1s = {}
103        # Mapping of trans_id -> new file_id
104        self._new_id = {}
105        # Mapping of old file-id -> trans_id
106        self._non_present_ids = {}
107        # Mapping of new file_id -> trans_id
108        self._r_new_id = {}
109        # The trans_id that will be used as the tree root
110        if tree.is_versioned(''):
111            self._new_root = self.trans_id_tree_path('')
112        else:
113            self._new_root = None
114        # Whether the target is case sensitive
115        self._case_sensitive_target = case_sensitive
116
117    def finalize(self):
118        """Release the working tree lock, if held.
119
120        This is required if apply has not been invoked, but can be invoked
121        even after apply.
122        """
123        if self._tree is None:
124            return
125        for hook in MutableTree.hooks['post_transform']:
126            hook(self._tree, self)
127        self._tree.unlock()
128        self._tree = None
129
130    def __get_root(self):
131        return self._new_root
132
133    root = property(__get_root)
134
135    def create_path(self, name, parent):
136        """Assign a transaction id to a new path"""
137        trans_id = self.assign_id()
138        unique_add(self._new_name, trans_id, name)
139        unique_add(self._new_parent, trans_id, parent)
140        return trans_id
141
142    def adjust_root_path(self, name, parent):
143        """Emulate moving the root by moving all children, instead.
144
145        We do this by undoing the association of root's transaction id with the
146        current tree.  This allows us to create a new directory with that
147        transaction id.  We unversion the root directory and version the
148        physically new directory, and hope someone versions the tree root
149        later.
150        """
151        old_root = self._new_root
152        old_root_file_id = self.final_file_id(old_root)
153        # force moving all children of root
154        for child_id in self.iter_tree_children(old_root):
155            if child_id != parent:
156                self.adjust_path(self.final_name(child_id),
157                                 self.final_parent(child_id), child_id)
158            file_id = self.final_file_id(child_id)
159            if file_id is not None:
160                self.unversion_file(child_id)
161            self.version_file(child_id, file_id=file_id)
162
163        # the physical root needs a new transaction id
164        self._tree_path_ids.pop("")
165        self._tree_id_paths.pop(old_root)
166        self._new_root = self.trans_id_tree_path('')
167        if parent == old_root:
168            parent = self._new_root
169        self.adjust_path(name, parent, old_root)
170        self.create_directory(old_root)
171        self.version_file(old_root, file_id=old_root_file_id)
172        self.unversion_file(self._new_root)
173
174    def fixup_new_roots(self):
175        """Reinterpret requests to change the root directory
176
177        Instead of creating a root directory, or moving an existing directory,
178        all the attributes and children of the new root are applied to the
179        existing root directory.
180
181        This means that the old root trans-id becomes obsolete, so it is
182        recommended only to invoke this after the root trans-id has become
183        irrelevant.
184
185        """
186        new_roots = [k for k, v in self._new_parent.items()
187                     if v == ROOT_PARENT]
188        if len(new_roots) < 1:
189            return
190        if len(new_roots) != 1:
191            raise ValueError('A tree cannot have two roots!')
192        if self._new_root is None:
193            self._new_root = new_roots[0]
194            return
195        old_new_root = new_roots[0]
196        # unversion the new root's directory.
197        if self.final_kind(self._new_root) is None:
198            file_id = self.final_file_id(old_new_root)
199        else:
200            file_id = self.final_file_id(self._new_root)
201        if old_new_root in self._new_id:
202            self.cancel_versioning(old_new_root)
203        else:
204            self.unversion_file(old_new_root)
205        # if, at this stage, root still has an old file_id, zap it so we can
206        # stick a new one in.
207        if (self.tree_file_id(self._new_root) is not None
208                and self._new_root not in self._removed_id):
209            self.unversion_file(self._new_root)
210        if file_id is not None:
211            self.version_file(self._new_root, file_id=file_id)
212
213        # Now move children of new root into old root directory.
214        # Ensure all children are registered with the transaction, but don't
215        # use directly-- some tree children have new parents
216        list(self.iter_tree_children(old_new_root))
217        # Move all children of new root into old root directory.
218        for child in self.by_parent().get(old_new_root, []):
219            self.adjust_path(self.final_name(child), self._new_root, child)
220
221        # Ensure old_new_root has no directory.
222        if old_new_root in self._new_contents:
223            self.cancel_creation(old_new_root)
224        else:
225            self.delete_contents(old_new_root)
226
227        # prevent deletion of root directory.
228        if self._new_root in self._removed_contents:
229            self.cancel_deletion(self._new_root)
230
231        # destroy path info for old_new_root.
232        del self._new_parent[old_new_root]
233        del self._new_name[old_new_root]
234
235    def trans_id_file_id(self, file_id):
236        """Determine or set the transaction id associated with a file ID.
237        A new id is only created for file_ids that were never present.  If
238        a transaction has been unversioned, it is deliberately still returned.
239        (this will likely lead to an unversioned parent conflict.)
240        """
241        if file_id is None:
242            raise ValueError('None is not a valid file id')
243        if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
244            return self._r_new_id[file_id]
245        else:
246            try:
247                path = self._tree.id2path(file_id)
248            except errors.NoSuchId:
249                if file_id in self._non_present_ids:
250                    return self._non_present_ids[file_id]
251                else:
252                    trans_id = self.assign_id()
253                    self._non_present_ids[file_id] = trans_id
254                    return trans_id
255            else:
256                return self.trans_id_tree_path(path)
257
258    def version_file(self, trans_id, file_id=None):
259        """Schedule a file to become versioned."""
260        raise NotImplementedError(self.version_file)
261
262    def cancel_versioning(self, trans_id):
263        """Undo a previous versioning of a file"""
264        raise NotImplementedError(self.cancel_versioning)
265
266    def new_paths(self, filesystem_only=False):
267        """Determine the paths of all new and changed files.
268
269        :param filesystem_only: if True, only calculate values for files
270            that require renames or execute bit changes.
271        """
272        new_ids = set()
273        if filesystem_only:
274            stale_ids = self._needs_rename.difference(self._new_name)
275            stale_ids.difference_update(self._new_parent)
276            stale_ids.difference_update(self._new_contents)
277            stale_ids.difference_update(self._new_id)
278            needs_rename = self._needs_rename.difference(stale_ids)
279            id_sets = (needs_rename, self._new_executability)
280        else:
281            id_sets = (self._new_name, self._new_parent, self._new_contents,
282                       self._new_id, self._new_executability)
283        for id_set in id_sets:
284            new_ids.update(id_set)
285        return sorted(FinalPaths(self).get_paths(new_ids))
286
287    def tree_file_id(self, trans_id):
288        """Determine the file id associated with the trans_id in the tree"""
289        path = self.tree_path(trans_id)
290        if path is None:
291            return None
292        # the file is old; the old id is still valid
293        if self._new_root == trans_id:
294            return self._tree.path2id('')
295        return self._tree.path2id(path)
296
297    def final_is_versioned(self, trans_id):
298        return self.final_file_id(trans_id) is not None
299
300    def final_file_id(self, trans_id):
301        """Determine the file id after any changes are applied, or None.
302
303        None indicates that the file will not be versioned after changes are
304        applied.
305        """
306        try:
307            return self._new_id[trans_id]
308        except KeyError:
309            if trans_id in self._removed_id:
310                return None
311        return self.tree_file_id(trans_id)
312
313    def inactive_file_id(self, trans_id):
314        """Return the inactive file_id associated with a transaction id.
315        That is, the one in the tree or in non_present_ids.
316        The file_id may actually be active, too.
317        """
318        file_id = self.tree_file_id(trans_id)
319        if file_id is not None:
320            return file_id
321        for key, value in self._non_present_ids.items():
322            if value == trans_id:
323                return key
324
325    def find_raw_conflicts(self):
326        """Find any violations of inventory or filesystem invariants"""
327        if self._done is True:
328            raise ReusingTransform()
329        conflicts = []
330        # ensure all children of all existent parents are known
331        # all children of non-existent parents are known, by definition.
332        self._add_tree_children()
333        by_parent = self.by_parent()
334        conflicts.extend(self._unversioned_parents(by_parent))
335        conflicts.extend(self._parent_loops())
336        conflicts.extend(self._duplicate_entries(by_parent))
337        conflicts.extend(self._parent_type_conflicts(by_parent))
338        conflicts.extend(self._improper_versioning())
339        conflicts.extend(self._executability_conflicts())
340        conflicts.extend(self._overwrite_conflicts())
341        return conflicts
342
343    def _check_malformed(self):
344        conflicts = self.find_raw_conflicts()
345        if len(conflicts) != 0:
346            raise MalformedTransform(conflicts=conflicts)
347
348    def _add_tree_children(self):
349        """Add all the children of all active parents to the known paths.
350
351        Active parents are those which gain children, and those which are
352        removed.  This is a necessary first step in detecting conflicts.
353        """
354        parents = list(self.by_parent())
355        parents.extend([t for t in self._removed_contents if
356                        self.tree_kind(t) == 'directory'])
357        for trans_id in self._removed_id:
358            path = self.tree_path(trans_id)
359            if path is not None:
360                if self._tree.stored_kind(path) == 'directory':
361                    parents.append(trans_id)
362            elif self.tree_kind(trans_id) == 'directory':
363                parents.append(trans_id)
364
365        for parent_id in parents:
366            # ensure that all children are registered with the transaction
367            list(self.iter_tree_children(parent_id))
368
369    def _has_named_child(self, name, parent_id, known_children):
370        """Does a parent already have a name child.
371
372        :param name: The searched for name.
373
374        :param parent_id: The parent for which the check is made.
375
376        :param known_children: The already known children. This should have
377            been recently obtained from `self.by_parent.get(parent_id)`
378            (or will be if None is passed).
379        """
380        if known_children is None:
381            known_children = self.by_parent().get(parent_id, [])
382        for child in known_children:
383            if self.final_name(child) == name:
384                return True
385        parent_path = self._tree_id_paths.get(parent_id, None)
386        if parent_path is None:
387            # No parent... no children
388            return False
389        child_path = joinpath(parent_path, name)
390        child_id = self._tree_path_ids.get(child_path, None)
391        if child_id is None:
392            # Not known by the tree transform yet, check the filesystem
393            return osutils.lexists(self._tree.abspath(child_path))
394        else:
395            raise AssertionError('child_id is missing: %s, %s, %s'
396                                 % (name, parent_id, child_id))
397
398    def _available_backup_name(self, name, target_id):
399        """Find an available backup name.
400
401        :param name: The basename of the file.
402
403        :param target_id: The directory trans_id where the backup should
404            be placed.
405        """
406        known_children = self.by_parent().get(target_id, [])
407        return osutils.available_backup_name(
408            name,
409            lambda base: self._has_named_child(
410                base, target_id, known_children))
411
412    def _parent_loops(self):
413        """No entry should be its own ancestor"""
414        for trans_id in self._new_parent:
415            seen = set()
416            parent_id = trans_id
417            while parent_id != ROOT_PARENT:
418                seen.add(parent_id)
419                try:
420                    parent_id = self.final_parent(parent_id)
421                except KeyError:
422                    break
423                if parent_id == trans_id:
424                    yield ('parent loop', trans_id)
425                if parent_id in seen:
426                    break
427
428    def _unversioned_parents(self, by_parent):
429        """If parent directories are versioned, children must be versioned."""
430        for parent_id, children in by_parent.items():
431            if parent_id == ROOT_PARENT:
432                continue
433            if self.final_is_versioned(parent_id):
434                continue
435            for child_id in children:
436                if self.final_is_versioned(child_id):
437                    yield ('unversioned parent', parent_id)
438                    break
439
440    def _improper_versioning(self):
441        """Cannot version a file with no contents, or a bad type.
442
443        However, existing entries with no contents are okay.
444        """
445        for trans_id in self._new_id:
446            kind = self.final_kind(trans_id)
447            if kind == 'symlink' and not self._tree.supports_symlinks():
448                # Ignore symlinks as they are not supported on this platform
449                continue
450            if kind is None:
451                yield ('versioning no contents', trans_id)
452                continue
453            if not self._tree.versionable_kind(kind):
454                yield ('versioning bad kind', trans_id, kind)
455
456    def _executability_conflicts(self):
457        """Check for bad executability changes.
458
459        Only versioned files may have their executability set, because
460        1. only versioned entries can have executability under windows
461        2. only files can be executable.  (The execute bit on a directory
462           does not indicate searchability)
463        """
464        for trans_id in self._new_executability:
465            if not self.final_is_versioned(trans_id):
466                yield ('unversioned executability', trans_id)
467            else:
468                if self.final_kind(trans_id) != "file":
469                    yield ('non-file executability', trans_id)
470
471    def _overwrite_conflicts(self):
472        """Check for overwrites (not permitted on Win32)"""
473        for trans_id in self._new_contents:
474            if self.tree_kind(trans_id) is None:
475                continue
476            if trans_id not in self._removed_contents:
477                yield ('overwrite', trans_id, self.final_name(trans_id))
478
479    def _duplicate_entries(self, by_parent):
480        """No directory may have two entries with the same name."""
481        if (self._new_name, self._new_parent) == ({}, {}):
482            return
483        for children in by_parent.values():
484            name_ids = []
485            for child_tid in children:
486                name = self.final_name(child_tid)
487                if name is not None:
488                    # Keep children only if they still exist in the end
489                    if not self._case_sensitive_target:
490                        name = name.lower()
491                    name_ids.append((name, child_tid))
492            name_ids.sort()
493            last_name = None
494            last_trans_id = None
495            for name, trans_id in name_ids:
496                kind = self.final_kind(trans_id)
497                if kind is None and not self.final_is_versioned(trans_id):
498                    continue
499                if name == last_name:
500                    yield ('duplicate', last_trans_id, trans_id, name)
501                last_name = name
502                last_trans_id = trans_id
503
504    def _parent_type_conflicts(self, by_parent):
505        """Children must have a directory parent"""
506        for parent_id, children in by_parent.items():
507            if parent_id == ROOT_PARENT:
508                continue
509            no_children = True
510            for child_id in children:
511                if self.final_kind(child_id) is not None:
512                    no_children = False
513                    break
514            if no_children:
515                continue
516            # There is at least a child, so we need an existing directory to
517            # contain it.
518            kind = self.final_kind(parent_id)
519            if kind is None:
520                # The directory will be deleted
521                yield ('missing parent', parent_id)
522            elif kind != "directory":
523                # Meh, we need a *directory* to put something in it
524                yield ('non-directory parent', parent_id)
525
526    def _set_executability(self, path, trans_id):
527        """Set the executability of versioned files """
528        if self._tree._supports_executable():
529            new_executability = self._new_executability[trans_id]
530            abspath = self._tree.abspath(path)
531            current_mode = os.stat(abspath).st_mode
532            if new_executability:
533                umask = os.umask(0)
534                os.umask(umask)
535                to_mode = current_mode | (0o100 & ~umask)
536                # Enable x-bit for others only if they can read it.
537                if current_mode & 0o004:
538                    to_mode |= 0o001 & ~umask
539                if current_mode & 0o040:
540                    to_mode |= 0o010 & ~umask
541            else:
542                to_mode = current_mode & ~0o111
543            osutils.chmod_if_possible(abspath, to_mode)
544
545    def _new_entry(self, name, parent_id, file_id):
546        """Helper function to create a new filesystem entry."""
547        trans_id = self.create_path(name, parent_id)
548        if file_id is not None:
549            self.version_file(trans_id, file_id=file_id)
550        return trans_id
551
552    def new_file(self, name, parent_id, contents, file_id=None,
553                 executable=None, sha1=None):
554        """Convenience method to create files.
555
556        name is the name of the file to create.
557        parent_id is the transaction id of the parent directory of the file.
558        contents is an iterator of bytestrings, which will be used to produce
559        the file.
560        :param file_id: The inventory ID of the file, if it is to be versioned.
561        :param executable: Only valid when a file_id has been supplied.
562        """
563        trans_id = self._new_entry(name, parent_id, file_id)
564        # TODO: rather than scheduling a set_executable call,
565        # have create_file create the file with the right mode.
566        self.create_file(contents, trans_id, sha1=sha1)
567        if executable is not None:
568            self.set_executability(executable, trans_id)
569        return trans_id
570
571    def new_directory(self, name, parent_id, file_id=None):
572        """Convenience method to create directories.
573
574        name is the name of the directory to create.
575        parent_id is the transaction id of the parent directory of the
576        directory.
577        file_id is the inventory ID of the directory, if it is to be versioned.
578        """
579        trans_id = self._new_entry(name, parent_id, file_id)
580        self.create_directory(trans_id)
581        return trans_id
582
583    def new_symlink(self, name, parent_id, target, file_id=None):
584        """Convenience method to create symbolic link.
585
586        name is the name of the symlink to create.
587        parent_id is the transaction id of the parent directory of the symlink.
588        target is a bytestring of the target of the symlink.
589        file_id is the inventory ID of the file, if it is to be versioned.
590        """
591        trans_id = self._new_entry(name, parent_id, file_id)
592        self.create_symlink(target, trans_id)
593        return trans_id
594
595    def new_orphan(self, trans_id, parent_id):
596        """Schedule an item to be orphaned.
597
598        When a directory is about to be removed, its children, if they are not
599        versioned are moved out of the way: they don't have a parent anymore.
600
601        :param trans_id: The trans_id of the existing item.
602        :param parent_id: The parent trans_id of the item.
603        """
604        raise NotImplementedError(self.new_orphan)
605
606    def _get_potential_orphans(self, dir_id):
607        """Find the potential orphans in a directory.
608
609        A directory can't be safely deleted if there are versioned files in it.
610        If all the contained files are unversioned then they can be orphaned.
611
612        The 'None' return value means that the directory contains at least one
613        versioned file and should not be deleted.
614
615        :param dir_id: The directory trans id.
616
617        :return: A list of the orphan trans ids or None if at least one
618             versioned file is present.
619        """
620        orphans = []
621        # Find the potential orphans, stop if one item should be kept
622        for child_tid in self.by_parent()[dir_id]:
623            if child_tid in self._removed_contents:
624                # The child is removed as part of the transform. Since it was
625                # versioned before, it's not an orphan
626                continue
627            if not self.final_is_versioned(child_tid):
628                # The child is not versioned
629                orphans.append(child_tid)
630            else:
631                # We have a versioned file here, searching for orphans is
632                # meaningless.
633                orphans = None
634                break
635        return orphans
636
637    def _affected_ids(self):
638        """Return the set of transform ids affected by the transform"""
639        trans_ids = set(self._removed_id)
640        trans_ids.update(self._new_id)
641        trans_ids.update(self._removed_contents)
642        trans_ids.update(self._new_contents)
643        trans_ids.update(self._new_executability)
644        trans_ids.update(self._new_name)
645        trans_ids.update(self._new_parent)
646        return trans_ids
647
648    def _get_file_id_maps(self):
649        """Return mapping of file_ids to trans_ids in the to and from states"""
650        trans_ids = self._affected_ids()
651        from_trans_ids = {}
652        to_trans_ids = {}
653        # Build up two dicts: trans_ids associated with file ids in the
654        # FROM state, vs the TO state.
655        for trans_id in trans_ids:
656            from_file_id = self.tree_file_id(trans_id)
657            if from_file_id is not None:
658                from_trans_ids[from_file_id] = trans_id
659            to_file_id = self.final_file_id(trans_id)
660            if to_file_id is not None:
661                to_trans_ids[to_file_id] = trans_id
662        return from_trans_ids, to_trans_ids
663
664    def _from_file_data(self, from_trans_id, from_versioned, from_path):
665        """Get data about a file in the from (tree) state
666
667        Return a (name, parent, kind, executable) tuple
668        """
669        from_path = self._tree_id_paths.get(from_trans_id)
670        if from_versioned:
671            # get data from working tree if versioned
672            from_entry = next(self._tree.iter_entries_by_dir(
673                specific_files=[from_path]))[1]
674            from_name = from_entry.name
675            from_parent = from_entry.parent_id
676        else:
677            from_entry = None
678            if from_path is None:
679                # File does not exist in FROM state
680                from_name = None
681                from_parent = None
682            else:
683                # File exists, but is not versioned.  Have to use path-
684                # splitting stuff
685                from_name = os.path.basename(from_path)
686                tree_parent = self.get_tree_parent(from_trans_id)
687                from_parent = self.tree_file_id(tree_parent)
688        if from_path is not None:
689            from_kind, from_executable, from_stats = \
690                self._tree._comparison_data(from_entry, from_path)
691        else:
692            from_kind = None
693            from_executable = False
694        return from_name, from_parent, from_kind, from_executable
695
696    def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
697        """Get data about a file in the to (target) state
698
699        Return a (name, parent, kind, executable) tuple
700        """
701        to_name = self.final_name(to_trans_id)
702        to_kind = self.final_kind(to_trans_id)
703        to_parent = self.final_file_id(self.final_parent(to_trans_id))
704        if to_trans_id in self._new_executability:
705            to_executable = self._new_executability[to_trans_id]
706        elif to_trans_id == from_trans_id:
707            to_executable = from_executable
708        else:
709            to_executable = False
710        return to_name, to_parent, to_kind, to_executable
711
712    def iter_changes(self):
713        """Produce output in the same format as Tree.iter_changes.
714
715        Will produce nonsensical results if invoked while inventory/filesystem
716        conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
717
718        This reads the Transform, but only reproduces changes involving a
719        file_id.  Files that are not versioned in either of the FROM or TO
720        states are not reflected.
721        """
722        final_paths = FinalPaths(self)
723        from_trans_ids, to_trans_ids = self._get_file_id_maps()
724        results = []
725        # Now iterate through all active file_ids
726        for file_id in set(from_trans_ids).union(to_trans_ids):
727            modified = False
728            from_trans_id = from_trans_ids.get(file_id)
729            # find file ids, and determine versioning state
730            if from_trans_id is None:
731                from_versioned = False
732                from_trans_id = to_trans_ids[file_id]
733            else:
734                from_versioned = True
735            to_trans_id = to_trans_ids.get(file_id)
736            if to_trans_id is None:
737                to_versioned = False
738                to_trans_id = from_trans_id
739            else:
740                to_versioned = True
741
742            if not from_versioned:
743                from_path = None
744            else:
745                from_path = self._tree_id_paths.get(from_trans_id)
746            if not to_versioned:
747                to_path = None
748            else:
749                to_path = final_paths.get_path(to_trans_id)
750
751            from_name, from_parent, from_kind, from_executable = \
752                self._from_file_data(from_trans_id, from_versioned, from_path)
753
754            to_name, to_parent, to_kind, to_executable = \
755                self._to_file_data(to_trans_id, from_trans_id, from_executable)
756
757            if from_kind != to_kind:
758                modified = True
759            elif to_kind in ('file', 'symlink') and (
760                    to_trans_id != from_trans_id
761                    or to_trans_id in self._new_contents):
762                modified = True
763            if (not modified and from_versioned == to_versioned
764                and from_parent == to_parent and from_name == to_name
765                    and from_executable == to_executable):
766                continue
767            results.append(
768                inventorytree.InventoryTreeChange(
769                    file_id, (from_path, to_path), modified,
770                    (from_versioned, to_versioned),
771                    (from_parent, to_parent),
772                    (from_name, to_name),
773                    (from_kind, to_kind),
774                    (from_executable, to_executable)))
775
776        def path_key(c):
777            return (c.path[0] or '', c.path[1] or '')
778        return iter(sorted(results, key=path_key))
779
780    def get_preview_tree(self):
781        """Return a tree representing the result of the transform.
782
783        The tree is a snapshot, and altering the TreeTransform will invalidate
784        it.
785        """
786        raise NotImplementedError(self.get_preview)
787
788    def commit(self, branch, message, merge_parents=None, strict=False,
789               timestamp=None, timezone=None, committer=None, authors=None,
790               revprops=None, revision_id=None):
791        """Commit the result of this TreeTransform to a branch.
792
793        :param branch: The branch to commit to.
794        :param message: The message to attach to the commit.
795        :param merge_parents: Additional parent revision-ids specified by
796            pending merges.
797        :param strict: If True, abort the commit if there are unversioned
798            files.
799        :param timestamp: if not None, seconds-since-epoch for the time and
800            date.  (May be a float.)
801        :param timezone: Optional timezone for timestamp, as an offset in
802            seconds.
803        :param committer: Optional committer in email-id format.
804            (e.g. "J Random Hacker <jrandom@example.com>")
805        :param authors: Optional list of authors in email-id format.
806        :param revprops: Optional dictionary of revision properties.
807        :param revision_id: Optional revision id.  (Specifying a revision-id
808            may reduce performance for some non-native formats.)
809        :return: The revision_id of the revision committed.
810        """
811        self._check_malformed()
812        if strict:
813            unversioned = set(self._new_contents).difference(set(self._new_id))
814            for trans_id in unversioned:
815                if not self.final_is_versioned(trans_id):
816                    raise errors.StrictCommitFailed()
817
818        revno, last_rev_id = branch.last_revision_info()
819        if last_rev_id == _mod_revision.NULL_REVISION:
820            if merge_parents is not None:
821                raise ValueError('Cannot supply merge parents for first'
822                                 ' commit.')
823            parent_ids = []
824        else:
825            parent_ids = [last_rev_id]
826            if merge_parents is not None:
827                parent_ids.extend(merge_parents)
828        if self._tree.get_revision_id() != last_rev_id:
829            raise ValueError('TreeTransform not based on branch basis: %s' %
830                             self._tree.get_revision_id().decode('utf-8'))
831        from .. import commit
832        revprops = commit.Commit.update_revprops(revprops, branch, authors)
833        builder = branch.get_commit_builder(parent_ids,
834                                            timestamp=timestamp,
835                                            timezone=timezone,
836                                            committer=committer,
837                                            revprops=revprops,
838                                            revision_id=revision_id)
839        preview = self.get_preview_tree()
840        list(builder.record_iter_changes(preview, last_rev_id,
841                                         self.iter_changes()))
842        builder.finish_inventory()
843        revision_id = builder.commit(message)
844        branch.set_last_revision_info(revno + 1, revision_id)
845        return revision_id
846
847    def _text_parent(self, trans_id):
848        path = self.tree_path(trans_id)
849        try:
850            if path is None or self._tree.kind(path) != 'file':
851                return None
852        except errors.NoSuchFile:
853            return None
854        return path
855
856    def _get_parents_texts(self, trans_id):
857        """Get texts for compression parents of this file."""
858        path = self._text_parent(trans_id)
859        if path is None:
860            return ()
861        return (self._tree.get_file_text(path),)
862
863    def _get_parents_lines(self, trans_id):
864        """Get lines for compression parents of this file."""
865        path = self._text_parent(trans_id)
866        if path is None:
867            return ()
868        return (self._tree.get_file_lines(path),)
869
870    def serialize(self, serializer):
871        """Serialize this TreeTransform.
872
873        :param serializer: A Serialiser like pack.ContainerSerializer.
874        """
875        from .. import bencode
876        new_name = {k.encode('utf-8'): v.encode('utf-8')
877                    for k, v in self._new_name.items()}
878        new_parent = {k.encode('utf-8'): v.encode('utf-8')
879                      for k, v in self._new_parent.items()}
880        new_id = {k.encode('utf-8'): v
881                  for k, v in self._new_id.items()}
882        new_executability = {k.encode('utf-8'): int(v)
883                             for k, v in self._new_executability.items()}
884        tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
885                         for k, v in self._tree_path_ids.items()}
886        non_present_ids = {k: v.encode('utf-8')
887                           for k, v in self._non_present_ids.items()}
888        removed_contents = [trans_id.encode('utf-8')
889                            for trans_id in self._removed_contents]
890        removed_id = [trans_id.encode('utf-8')
891                      for trans_id in self._removed_id]
892        attribs = {
893            b'_id_number': self._id_number,
894            b'_new_name': new_name,
895            b'_new_parent': new_parent,
896            b'_new_executability': new_executability,
897            b'_new_id': new_id,
898            b'_tree_path_ids': tree_path_ids,
899            b'_removed_id': removed_id,
900            b'_removed_contents': removed_contents,
901            b'_non_present_ids': non_present_ids,
902            }
903        yield serializer.bytes_record(bencode.bencode(attribs),
904                                      ((b'attribs',),))
905        for trans_id, kind in sorted(self._new_contents.items()):
906            if kind == 'file':
907                with open(self._limbo_name(trans_id), 'rb') as cur_file:
908                    lines = cur_file.readlines()
909                parents = self._get_parents_lines(trans_id)
910                mpdiff = multiparent.MultiParent.from_lines(lines, parents)
911                content = b''.join(mpdiff.to_patch())
912            if kind == 'directory':
913                content = b''
914            if kind == 'symlink':
915                content = self._read_symlink_target(trans_id)
916                if not isinstance(content, bytes):
917                    content = content.encode('utf-8')
918            yield serializer.bytes_record(
919                content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
920
921    def deserialize(self, records):
922        """Deserialize a stored TreeTransform.
923
924        :param records: An iterable of (names, content) tuples, as per
925            pack.ContainerPushParser.
926        """
927        from .. import bencode
928        names, content = next(records)
929        attribs = bencode.bdecode(content)
930        self._id_number = attribs[b'_id_number']
931        self._new_name = {k.decode('utf-8'): v.decode('utf-8')
932                          for k, v in attribs[b'_new_name'].items()}
933        self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
934                            for k, v in attribs[b'_new_parent'].items()}
935        self._new_executability = {
936            k.decode('utf-8'): bool(v)
937            for k, v in attribs[b'_new_executability'].items()}
938        self._new_id = {k.decode('utf-8'): v
939                        for k, v in attribs[b'_new_id'].items()}
940        self._r_new_id = {v: k for k, v in self._new_id.items()}
941        self._tree_path_ids = {}
942        self._tree_id_paths = {}
943        for bytepath, trans_id in attribs[b'_tree_path_ids'].items():
944            path = bytepath.decode('utf-8')
945            trans_id = trans_id.decode('utf-8')
946            self._tree_path_ids[path] = trans_id
947            self._tree_id_paths[trans_id] = path
948        self._removed_id = {trans_id.decode('utf-8')
949                            for trans_id in attribs[b'_removed_id']}
950        self._removed_contents = set(
951            trans_id.decode('utf-8')
952            for trans_id in attribs[b'_removed_contents'])
953        self._non_present_ids = {
954            k: v.decode('utf-8')
955            for k, v in attribs[b'_non_present_ids'].items()}
956        for ((trans_id, kind),), content in records:
957            trans_id = trans_id.decode('utf-8')
958            kind = kind.decode('ascii')
959            if kind == 'file':
960                mpdiff = multiparent.MultiParent.from_patch(content)
961                lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
962                self.create_file(lines, trans_id)
963            if kind == 'directory':
964                self.create_directory(trans_id)
965            if kind == 'symlink':
966                self.create_symlink(content.decode('utf-8'), trans_id)
967
968    def create_file(self, contents, trans_id, mode_id=None, sha1=None):
969        """Schedule creation of a new file.
970
971        :seealso: new_file.
972
973        :param contents: an iterator of strings, all of which will be written
974            to the target destination.
975        :param trans_id: TreeTransform handle
976        :param mode_id: If not None, force the mode of the target file to match
977            the mode of the object referenced by mode_id.
978            Otherwise, we will try to preserve mode bits of an existing file.
979        :param sha1: If the sha1 of this content is already known, pass it in.
980            We can use it to prevent future sha1 computations.
981        """
982        raise NotImplementedError(self.create_file)
983
984    def create_directory(self, trans_id):
985        """Schedule creation of a new directory.
986
987        See also new_directory.
988        """
989        raise NotImplementedError(self.create_directory)
990
991    def create_symlink(self, target, trans_id):
992        """Schedule creation of a new symbolic link.
993
994        target is a bytestring.
995        See also new_symlink.
996        """
997        raise NotImplementedError(self.create_symlink)
998
999    def create_hardlink(self, path, trans_id):
1000        """Schedule creation of a hard link"""
1001        raise NotImplementedError(self.create_hardlink)
1002
1003    def cancel_creation(self, trans_id):
1004        """Cancel the creation of new file contents."""
1005        raise NotImplementedError(self.cancel_creation)
1006
1007    def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1008        """Apply all changes to the inventory and filesystem.
1009
1010        If filesystem or inventory conflicts are present, MalformedTransform
1011        will be thrown.
1012
1013        If apply succeeds, finalize is not necessary.
1014
1015        :param no_conflicts: if True, the caller guarantees there are no
1016            conflicts, so no check is made.
1017        :param precomputed_delta: An inventory delta to use instead of
1018            calculating one.
1019        :param _mover: Supply an alternate FileMover, for testing
1020        """
1021        raise NotImplementedError(self.apply)
1022
1023    def cook_conflicts(self, raw_conflicts):
1024        """Generate a list of cooked conflicts, sorted by file path"""
1025        content_conflict_file_ids = set()
1026        cooked_conflicts = list(iter_cook_conflicts(raw_conflicts, self))
1027        for c in cooked_conflicts:
1028            if c.typestring == 'contents conflict':
1029                content_conflict_file_ids.add(c.file_id)
1030        # We want to get rid of path conflicts when a corresponding contents
1031        # conflict exists. This can occur when one branch deletes a file while
1032        # the other renames *and* modifies it. In this case, the content
1033        # conflict is enough.
1034        cooked_conflicts = [
1035            c for c in cooked_conflicts
1036            if c.typestring != 'path conflict' or
1037            c.file_id not in content_conflict_file_ids]
1038        return sorted(cooked_conflicts, key=Conflict.sort_key)
1039
1040
1041def cook_path_conflict(
1042        tt, fp, conflict_type, trans_id, file_id, this_parent, this_name,
1043        other_parent, other_name):
1044    if this_parent is None or this_name is None:
1045        this_path = '<deleted>'
1046    else:
1047        parent_path = fp.get_path(tt.trans_id_file_id(this_parent))
1048        this_path = osutils.pathjoin(parent_path, this_name)
1049    if other_parent is None or other_name is None:
1050        other_path = '<deleted>'
1051    else:
1052        try:
1053            parent_path = fp.get_path(tt.trans_id_file_id(other_parent))
1054        except NoFinalPath:
1055            # The other entry was in a path that doesn't exist in our tree.
1056            # Put it in the root.
1057            parent_path = ''
1058        other_path = osutils.pathjoin(parent_path, other_name)
1059    return Conflict.factory(
1060        conflict_type, path=this_path,
1061        conflict_path=other_path,
1062        file_id=file_id)
1063
1064
1065def cook_content_conflict(tt, fp, conflict_type, trans_ids):
1066    for trans_id in trans_ids:
1067        file_id = tt.final_file_id(trans_id)
1068        if file_id is not None:
1069            # Ok we found the relevant file-id
1070            break
1071    path = fp.get_path(trans_id)
1072    for suffix in ('.BASE', '.THIS', '.OTHER'):
1073        if path.endswith(suffix):
1074            # Here is the raw path
1075            path = path[:-len(suffix)]
1076            break
1077    return Conflict.factory(conflict_type, path=path, file_id=file_id)
1078
1079
1080def cook_text_conflict(tt, fp, conflict_type, trans_id):
1081    path = fp.get_path(trans_id)
1082    file_id = tt.final_file_id(trans_id)
1083    return Conflict.factory(conflict_type, path=path, file_id=file_id)
1084
1085
1086CONFLICT_COOKERS = {
1087    'path conflict': cook_path_conflict,
1088    'text conflict': cook_text_conflict,
1089    'contents conflict': cook_content_conflict,
1090}
1091
1092def iter_cook_conflicts(raw_conflicts, tt):
1093    fp = FinalPaths(tt)
1094    for conflict in raw_conflicts:
1095        c_type = conflict[0]
1096        try:
1097            cooker = CONFLICT_COOKERS[c_type]
1098        except KeyError:
1099            action = conflict[1]
1100            modified_path = fp.get_path(conflict[2])
1101            modified_id = tt.final_file_id(conflict[2])
1102            if len(conflict) == 3:
1103                yield Conflict.factory(
1104                    c_type, action=action, path=modified_path, file_id=modified_id)
1105
1106            else:
1107                conflicting_path = fp.get_path(conflict[3])
1108                conflicting_id = tt.final_file_id(conflict[3])
1109                yield Conflict.factory(
1110                    c_type, action=action, path=modified_path,
1111                    file_id=modified_id,
1112                    conflict_path=conflicting_path,
1113                    conflict_file_id=conflicting_id)
1114        else:
1115            yield cooker(tt, fp, *conflict)
1116
1117
1118class DiskTreeTransform(TreeTransformBase):
1119    """Tree transform storing its contents on disk."""
1120
1121    def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
1122        """Constructor.
1123        :param tree: The tree that will be transformed, but not necessarily
1124            the output tree.
1125        :param limbodir: A directory where new files can be stored until
1126            they are installed in their proper places
1127        :param pb: ignored
1128        :param case_sensitive: If True, the target of the transform is
1129            case sensitive, not just case preserving.
1130        """
1131        TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1132        self._limbodir = limbodir
1133        self._deletiondir = None
1134        # A mapping of transform ids to their limbo filename
1135        self._limbo_files = {}
1136        self._possibly_stale_limbo_files = set()
1137        # A mapping of transform ids to a set of the transform ids of children
1138        # that their limbo directory has
1139        self._limbo_children = {}
1140        # Map transform ids to maps of child filename to child transform id
1141        self._limbo_children_names = {}
1142        # List of transform ids that need to be renamed from limbo into place
1143        self._needs_rename = set()
1144        self._creation_mtime = None
1145        self._create_symlinks = osutils.supports_symlinks(self._limbodir)
1146
1147    def finalize(self):
1148        """Release the working tree lock, if held, clean up limbo dir.
1149
1150        This is required if apply has not been invoked, but can be invoked
1151        even after apply.
1152        """
1153        if self._tree is None:
1154            return
1155        try:
1156            limbo_paths = list(self._limbo_files.values())
1157            limbo_paths.extend(self._possibly_stale_limbo_files)
1158            limbo_paths.sort(reverse=True)
1159            for path in limbo_paths:
1160                try:
1161                    osutils.delete_any(path)
1162                except OSError as e:
1163                    if e.errno != errno.ENOENT:
1164                        raise
1165                    # XXX: warn? perhaps we just got interrupted at an
1166                    # inconvenient moment, but perhaps files are disappearing
1167                    # from under us?
1168            try:
1169                osutils.delete_any(self._limbodir)
1170            except OSError:
1171                # We don't especially care *why* the dir is immortal.
1172                raise ImmortalLimbo(self._limbodir)
1173            try:
1174                if self._deletiondir is not None:
1175                    osutils.delete_any(self._deletiondir)
1176            except OSError:
1177                raise errors.ImmortalPendingDeletion(self._deletiondir)
1178        finally:
1179            TreeTransformBase.finalize(self)
1180
1181    def _limbo_supports_executable(self):
1182        """Check if the limbo path supports the executable bit."""
1183        return osutils.supports_executable(self._limbodir)
1184
1185    def _limbo_name(self, trans_id):
1186        """Generate the limbo name of a file"""
1187        limbo_name = self._limbo_files.get(trans_id)
1188        if limbo_name is None:
1189            limbo_name = self._generate_limbo_path(trans_id)
1190            self._limbo_files[trans_id] = limbo_name
1191        return limbo_name
1192
1193    def _generate_limbo_path(self, trans_id):
1194        """Generate a limbo path using the trans_id as the relative path.
1195
1196        This is suitable as a fallback, and when the transform should not be
1197        sensitive to the path encoding of the limbo directory.
1198        """
1199        self._needs_rename.add(trans_id)
1200        return osutils.pathjoin(self._limbodir, trans_id)
1201
1202    def adjust_path(self, name, parent, trans_id):
1203        previous_parent = self._new_parent.get(trans_id)
1204        previous_name = self._new_name.get(trans_id)
1205        super(DiskTreeTransform, self).adjust_path(name, parent, trans_id)
1206        if (trans_id in self._limbo_files
1207                and trans_id not in self._needs_rename):
1208            self._rename_in_limbo([trans_id])
1209            if previous_parent != parent:
1210                self._limbo_children[previous_parent].remove(trans_id)
1211            if previous_parent != parent or previous_name != name:
1212                del self._limbo_children_names[previous_parent][previous_name]
1213
1214    def _rename_in_limbo(self, trans_ids):
1215        """Fix limbo names so that the right final path is produced.
1216
1217        This means we outsmarted ourselves-- we tried to avoid renaming
1218        these files later by creating them with their final names in their
1219        final parents.  But now the previous name or parent is no longer
1220        suitable, so we have to rename them.
1221
1222        Even for trans_ids that have no new contents, we must remove their
1223        entries from _limbo_files, because they are now stale.
1224        """
1225        for trans_id in trans_ids:
1226            old_path = self._limbo_files[trans_id]
1227            self._possibly_stale_limbo_files.add(old_path)
1228            del self._limbo_files[trans_id]
1229            if trans_id not in self._new_contents:
1230                continue
1231            new_path = self._limbo_name(trans_id)
1232            os.rename(old_path, new_path)
1233            self._possibly_stale_limbo_files.remove(old_path)
1234            for descendant in self._limbo_descendants(trans_id):
1235                desc_path = self._limbo_files[descendant]
1236                desc_path = new_path + desc_path[len(old_path):]
1237                self._limbo_files[descendant] = desc_path
1238
1239    def _limbo_descendants(self, trans_id):
1240        """Return the set of trans_ids whose limbo paths descend from this."""
1241        descendants = set(self._limbo_children.get(trans_id, []))
1242        for descendant in list(descendants):
1243            descendants.update(self._limbo_descendants(descendant))
1244        return descendants
1245
1246    def _set_mode(self, trans_id, mode_id, typefunc):
1247        raise NotImplementedError(self._set_mode)
1248
1249    def create_file(self, contents, trans_id, mode_id=None, sha1=None):
1250        """Schedule creation of a new file.
1251
1252        :seealso: new_file.
1253
1254        :param contents: an iterator of strings, all of which will be written
1255            to the target destination.
1256        :param trans_id: TreeTransform handle
1257        :param mode_id: If not None, force the mode of the target file to match
1258            the mode of the object referenced by mode_id.
1259            Otherwise, we will try to preserve mode bits of an existing file.
1260        :param sha1: If the sha1 of this content is already known, pass it in.
1261            We can use it to prevent future sha1 computations.
1262        """
1263        name = self._limbo_name(trans_id)
1264        with open(name, 'wb') as f:
1265            unique_add(self._new_contents, trans_id, 'file')
1266            f.writelines(contents)
1267        self._set_mtime(name)
1268        self._set_mode(trans_id, mode_id, S_ISREG)
1269        # It is unfortunate we have to use lstat instead of fstat, but we just
1270        # used utime and chmod on the file, so we need the accurate final
1271        # details.
1272        if sha1 is not None:
1273            self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
1274
1275    def _read_symlink_target(self, trans_id):
1276        return os.readlink(self._limbo_name(trans_id))
1277
1278    def _set_mtime(self, path):
1279        """All files that are created get the same mtime.
1280
1281        This time is set by the first object to be created.
1282        """
1283        if self._creation_mtime is None:
1284            self._creation_mtime = time.time()
1285        os.utime(path, (self._creation_mtime, self._creation_mtime))
1286
1287    def create_hardlink(self, path, trans_id):
1288        """Schedule creation of a hard link"""
1289        name = self._limbo_name(trans_id)
1290        try:
1291            os.link(path, name)
1292        except OSError as e:
1293            if e.errno != errno.EPERM:
1294                raise
1295            raise errors.HardLinkNotSupported(path)
1296        try:
1297            unique_add(self._new_contents, trans_id, 'file')
1298        except BaseException:
1299            # Clean up the file, it never got registered so
1300            # TreeTransform.finalize() won't clean it up.
1301            os.unlink(name)
1302            raise
1303
1304    def create_directory(self, trans_id):
1305        """Schedule creation of a new directory.
1306
1307        See also new_directory.
1308        """
1309        os.mkdir(self._limbo_name(trans_id))
1310        unique_add(self._new_contents, trans_id, 'directory')
1311
1312    def create_symlink(self, target, trans_id):
1313        """Schedule creation of a new symbolic link.
1314
1315        target is a bytestring.
1316        See also new_symlink.
1317        """
1318        if self._create_symlinks:
1319            os.symlink(target, self._limbo_name(trans_id))
1320        else:
1321            try:
1322                path = FinalPaths(self).get_path(trans_id)
1323            except KeyError:
1324                path = None
1325            trace.warning(
1326                'Unable to create symlink "%s" on this filesystem.' % (path,))
1327        # We add symlink to _new_contents even if they are unsupported
1328        # and not created. These entries are subsequently used to avoid
1329        # conflicts on platforms that don't support symlink
1330        unique_add(self._new_contents, trans_id, 'symlink')
1331
1332    def cancel_creation(self, trans_id):
1333        """Cancel the creation of new file contents."""
1334        del self._new_contents[trans_id]
1335        if trans_id in self._observed_sha1s:
1336            del self._observed_sha1s[trans_id]
1337        children = self._limbo_children.get(trans_id)
1338        # if this is a limbo directory with children, move them before removing
1339        # the directory
1340        if children is not None:
1341            self._rename_in_limbo(children)
1342            del self._limbo_children[trans_id]
1343            del self._limbo_children_names[trans_id]
1344        osutils.delete_any(self._limbo_name(trans_id))
1345
1346    def new_orphan(self, trans_id, parent_id):
1347        conf = self._tree.get_config_stack()
1348        handle_orphan = conf.get('transform.orphan_policy')
1349        handle_orphan(self, trans_id, parent_id)
1350
1351
1352class InventoryTreeTransform(DiskTreeTransform):
1353    """Represent a tree transformation.
1354
1355    This object is designed to support incremental generation of the transform,
1356    in any order.
1357
1358    However, it gives optimum performance when parent directories are created
1359    before their contents.  The transform is then able to put child files
1360    directly in their parent directory, avoiding later renames.
1361
1362    It is easy to produce malformed transforms, but they are generally
1363    harmless.  Attempting to apply a malformed transform will cause an
1364    exception to be raised before any modifications are made to the tree.
1365
1366    Many kinds of malformed transforms can be corrected with the
1367    resolve_conflicts function.  The remaining ones indicate programming error,
1368    such as trying to create a file with no path.
1369
1370    Two sets of file creation methods are supplied.  Convenience methods are:
1371     * new_file
1372     * new_directory
1373     * new_symlink
1374
1375    These are composed of the low-level methods:
1376     * create_path
1377     * create_file or create_directory or create_symlink
1378     * version_file
1379     * set_executability
1380
1381    Transform/Transaction ids
1382    -------------------------
1383    trans_ids are temporary ids assigned to all files involved in a transform.
1384    It's possible, even common, that not all files in the Tree have trans_ids.
1385
1386    trans_ids are used because filenames and file_ids are not good enough
1387    identifiers; filenames change, and not all files have file_ids.  File-ids
1388    are also associated with trans-ids, so that moving a file moves its
1389    file-id.
1390
1391    trans_ids are only valid for the TreeTransform that generated them.
1392
1393    Limbo
1394    -----
1395    Limbo is a temporary directory use to hold new versions of files.
1396    Files are added to limbo by create_file, create_directory, create_symlink,
1397    and their convenience variants (new_*).  Files may be removed from limbo
1398    using cancel_creation.  Files are renamed from limbo into their final
1399    location as part of TreeTransform.apply
1400
1401    Limbo must be cleaned up, by either calling TreeTransform.apply or
1402    calling TreeTransform.finalize.
1403
1404    Files are placed into limbo inside their parent directories, where
1405    possible.  This reduces subsequent renames, and makes operations involving
1406    lots of files faster.  This optimization is only possible if the parent
1407    directory is created *before* creating any of its children, so avoid
1408    creating children before parents, where possible.
1409
1410    Pending-deletion
1411    ----------------
1412    This temporary directory is used by _FileMover for storing files that are
1413    about to be deleted.  In case of rollback, the files will be restored.
1414    FileMover does not delete files until it is sure that a rollback will not
1415    happen.
1416    """
1417
1418    def __init__(self, tree, pb=None):
1419        """Note: a tree_write lock is taken on the tree.
1420
1421        Use TreeTransform.finalize() to release the lock (can be omitted if
1422        TreeTransform.apply() called).
1423        """
1424        tree.lock_tree_write()
1425        try:
1426            limbodir = urlutils.local_path_from_url(
1427                tree._transport.abspath('limbo'))
1428            osutils.ensure_empty_directory_exists(
1429                limbodir,
1430                errors.ExistingLimbo)
1431            deletiondir = urlutils.local_path_from_url(
1432                tree._transport.abspath('pending-deletion'))
1433            osutils.ensure_empty_directory_exists(
1434                deletiondir,
1435                errors.ExistingPendingDeletion)
1436        except BaseException:
1437            tree.unlock()
1438            raise
1439
1440        # Cache of realpath results, to speed up canonical_path
1441        self._realpaths = {}
1442        # Cache of relpath results, to speed up canonical_path
1443        self._relpaths = {}
1444        DiskTreeTransform.__init__(self, tree, limbodir, pb,
1445                                   tree.case_sensitive)
1446        self._deletiondir = deletiondir
1447
1448    def canonical_path(self, path):
1449        """Get the canonical tree-relative path"""
1450        # don't follow final symlinks
1451        abs = self._tree.abspath(path)
1452        if abs in self._relpaths:
1453            return self._relpaths[abs]
1454        dirname, basename = os.path.split(abs)
1455        if dirname not in self._realpaths:
1456            self._realpaths[dirname] = os.path.realpath(dirname)
1457        dirname = self._realpaths[dirname]
1458        abs = osutils.pathjoin(dirname, basename)
1459        if dirname in self._relpaths:
1460            relpath = osutils.pathjoin(self._relpaths[dirname], basename)
1461            relpath = relpath.rstrip('/\\')
1462        else:
1463            relpath = self._tree.relpath(abs)
1464        self._relpaths[abs] = relpath
1465        return relpath
1466
1467    def tree_kind(self, trans_id):
1468        """Determine the file kind in the working tree.
1469
1470        :returns: The file kind or None if the file does not exist
1471        """
1472        path = self._tree_id_paths.get(trans_id)
1473        if path is None:
1474            return None
1475        try:
1476            return osutils.file_kind(self._tree.abspath(path))
1477        except errors.NoSuchFile:
1478            return None
1479
1480    def _set_mode(self, trans_id, mode_id, typefunc):
1481        """Set the mode of new file contents.
1482        The mode_id is the existing file to get the mode from (often the same
1483        as trans_id).  The operation is only performed if there's a mode match
1484        according to typefunc.
1485        """
1486        if mode_id is None:
1487            mode_id = trans_id
1488        try:
1489            old_path = self._tree_id_paths[mode_id]
1490        except KeyError:
1491            return
1492        try:
1493            mode = os.stat(self._tree.abspath(old_path)).st_mode
1494        except OSError as e:
1495            if e.errno in (errno.ENOENT, errno.ENOTDIR):
1496                # Either old_path doesn't exist, or the parent of the
1497                # target is not a directory (but will be one eventually)
1498                # Either way, we know it doesn't exist *right now*
1499                # See also bug #248448
1500                return
1501            else:
1502                raise
1503        if typefunc(mode):
1504            osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1505
1506    def iter_tree_children(self, parent_id):
1507        """Iterate through the entry's tree children, if any"""
1508        try:
1509            path = self._tree_id_paths[parent_id]
1510        except KeyError:
1511            return
1512        try:
1513            children = os.listdir(self._tree.abspath(path))
1514        except OSError as e:
1515            if not (osutils._is_error_enotdir(e) or
1516                    e.errno in (errno.ENOENT, errno.ESRCH)):
1517                raise
1518            return
1519
1520        for child in children:
1521            childpath = joinpath(path, child)
1522            if self._tree.is_control_filename(childpath):
1523                continue
1524            yield self.trans_id_tree_path(childpath)
1525
1526    def _generate_limbo_path(self, trans_id):
1527        """Generate a limbo path using the final path if possible.
1528
1529        This optimizes the performance of applying the tree transform by
1530        avoiding renames.  These renames can be avoided only when the parent
1531        directory is already scheduled for creation.
1532
1533        If the final path cannot be used, falls back to using the trans_id as
1534        the relpath.
1535        """
1536        parent = self._new_parent.get(trans_id)
1537        # if the parent directory is already in limbo (e.g. when building a
1538        # tree), choose a limbo name inside the parent, to reduce further
1539        # renames.
1540        use_direct_path = False
1541        if self._new_contents.get(parent) == 'directory':
1542            filename = self._new_name.get(trans_id)
1543            if filename is not None:
1544                if parent not in self._limbo_children:
1545                    self._limbo_children[parent] = set()
1546                    self._limbo_children_names[parent] = {}
1547                    use_direct_path = True
1548                # the direct path can only be used if no other file has
1549                # already taken this pathname, i.e. if the name is unused, or
1550                # if it is already associated with this trans_id.
1551                elif self._case_sensitive_target:
1552                    if (self._limbo_children_names[parent].get(filename)
1553                            in (trans_id, None)):
1554                        use_direct_path = True
1555                else:
1556                    for l_filename, l_trans_id in (
1557                            self._limbo_children_names[parent].items()):
1558                        if l_trans_id == trans_id:
1559                            continue
1560                        if l_filename.lower() == filename.lower():
1561                            break
1562                    else:
1563                        use_direct_path = True
1564
1565        if not use_direct_path:
1566            return DiskTreeTransform._generate_limbo_path(self, trans_id)
1567
1568        limbo_name = osutils.pathjoin(self._limbo_files[parent], filename)
1569        self._limbo_children[parent].add(trans_id)
1570        self._limbo_children_names[parent][filename] = trans_id
1571        return limbo_name
1572
1573    def version_file(self, trans_id, file_id=None):
1574        """Schedule a file to become versioned."""
1575        if file_id is None:
1576            raise ValueError()
1577        unique_add(self._new_id, trans_id, file_id)
1578        unique_add(self._r_new_id, file_id, trans_id)
1579
1580    def cancel_versioning(self, trans_id):
1581        """Undo a previous versioning of a file"""
1582        file_id = self._new_id[trans_id]
1583        del self._new_id[trans_id]
1584        del self._r_new_id[file_id]
1585
1586    def _duplicate_ids(self):
1587        """Each inventory id may only be used once"""
1588        conflicts = []
1589        try:
1590            all_ids = self._tree.all_file_ids()
1591        except errors.UnsupportedOperation:
1592            # it's okay for non-file-id trees to raise UnsupportedOperation.
1593            return []
1594        removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
1595                                self._removed_id))
1596        active_tree_ids = all_ids.difference(removed_tree_ids)
1597        for trans_id, file_id in self._new_id.items():
1598            if file_id in active_tree_ids:
1599                path = self._tree.id2path(file_id)
1600                old_trans_id = self.trans_id_tree_path(path)
1601                conflicts.append(('duplicate id', old_trans_id, trans_id))
1602        return conflicts
1603
1604    def find_raw_conflicts(self):
1605        conflicts = super(InventoryTreeTransform, self).find_raw_conflicts()
1606        conflicts.extend(self._duplicate_ids())
1607        return conflicts
1608
1609    def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1610        """Apply all changes to the inventory and filesystem.
1611
1612        If filesystem or inventory conflicts are present, MalformedTransform
1613        will be thrown.
1614
1615        If apply succeeds, finalize is not necessary.
1616
1617        :param no_conflicts: if True, the caller guarantees there are no
1618            conflicts, so no check is made.
1619        :param precomputed_delta: An inventory delta to use instead of
1620            calculating one.
1621        :param _mover: Supply an alternate FileMover, for testing
1622        """
1623        for hook in MutableTree.hooks['pre_transform']:
1624            hook(self._tree, self)
1625        if not no_conflicts:
1626            self._check_malformed()
1627        self.rename_count = 0
1628        with ui.ui_factory.nested_progress_bar() as child_pb:
1629            if precomputed_delta is None:
1630                child_pb.update(gettext('Apply phase'), 0, 2)
1631                inventory_delta = self._generate_inventory_delta()
1632                offset = 1
1633            else:
1634                inventory_delta = precomputed_delta
1635                offset = 0
1636            if _mover is None:
1637                mover = _FileMover()
1638            else:
1639                mover = _mover
1640            try:
1641                child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1642                self._apply_removals(mover)
1643                child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1644                modified_paths = self._apply_insertions(mover)
1645            except BaseException:
1646                mover.rollback()
1647                raise
1648            else:
1649                mover.apply_deletions()
1650        if self.final_file_id(self.root) is None:
1651            inventory_delta = [e for e in inventory_delta if e[0] != '']
1652        self._tree.apply_inventory_delta(inventory_delta)
1653        self._apply_observed_sha1s()
1654        self._done = True
1655        self.finalize()
1656        return _TransformResults(modified_paths, self.rename_count)
1657
1658    def _apply_removals(self, mover):
1659        """Perform tree operations that remove directory/inventory names.
1660
1661        That is, delete files that are to be deleted, and put any files that
1662        need renaming into limbo.  This must be done in strict child-to-parent
1663        order.
1664
1665        If inventory_delta is None, no inventory delta generation is performed.
1666        """
1667        tree_paths = sorted(self._tree_path_ids.items(), reverse=True)
1668        with ui.ui_factory.nested_progress_bar() as child_pb:
1669            for num, (path, trans_id) in enumerate(tree_paths):
1670                # do not attempt to move root into a subdirectory of itself.
1671                if path == '':
1672                    continue
1673                child_pb.update(gettext('removing file'), num, len(tree_paths))
1674                full_path = self._tree.abspath(path)
1675                if trans_id in self._removed_contents:
1676                    delete_path = os.path.join(self._deletiondir, trans_id)
1677                    mover.pre_delete(full_path, delete_path)
1678                elif (trans_id in self._new_name or
1679                      trans_id in self._new_parent):
1680                    try:
1681                        mover.rename(full_path, self._limbo_name(trans_id))
1682                    except TransformRenameFailed as e:
1683                        if e.errno != errno.ENOENT:
1684                            raise
1685                    else:
1686                        self.rename_count += 1
1687
1688    def _apply_insertions(self, mover):
1689        """Perform tree operations that insert directory/inventory names.
1690
1691        That is, create any files that need to be created, and restore from
1692        limbo any files that needed renaming.  This must be done in strict
1693        parent-to-child order.
1694
1695        If inventory_delta is None, no inventory delta is calculated, and
1696        no list of modified paths is returned.
1697        """
1698        new_paths = self.new_paths(filesystem_only=True)
1699        modified_paths = []
1700        with ui.ui_factory.nested_progress_bar() as child_pb:
1701            for num, (path, trans_id) in enumerate(new_paths):
1702                if (num % 10) == 0:
1703                    child_pb.update(gettext('adding file'),
1704                                    num, len(new_paths))
1705                full_path = self._tree.abspath(path)
1706                if trans_id in self._needs_rename:
1707                    try:
1708                        mover.rename(self._limbo_name(trans_id), full_path)
1709                    except TransformRenameFailed as e:
1710                        # We may be renaming a dangling inventory id
1711                        if e.errno != errno.ENOENT:
1712                            raise
1713                    else:
1714                        self.rename_count += 1
1715                    # TODO: if trans_id in self._observed_sha1s, we should
1716                    #       re-stat the final target, since ctime will be
1717                    #       updated by the change.
1718                if (trans_id in self._new_contents
1719                        or self.path_changed(trans_id)):
1720                    if trans_id in self._new_contents:
1721                        modified_paths.append(full_path)
1722                if trans_id in self._new_executability:
1723                    self._set_executability(path, trans_id)
1724                if trans_id in self._observed_sha1s:
1725                    o_sha1, o_st_val = self._observed_sha1s[trans_id]
1726                    st = osutils.lstat(full_path)
1727                    self._observed_sha1s[trans_id] = (o_sha1, st)
1728        for path, trans_id in new_paths:
1729            # new_paths includes stuff like workingtree conflicts. Only the
1730            # stuff in new_contents actually comes from limbo.
1731            if trans_id in self._limbo_files:
1732                del self._limbo_files[trans_id]
1733        self._new_contents.clear()
1734        return modified_paths
1735
1736    def _apply_observed_sha1s(self):
1737        """After we have finished renaming everything, update observed sha1s
1738
1739        This has to be done after self._tree.apply_inventory_delta, otherwise
1740        it doesn't know anything about the files we are updating. Also, we want
1741        to do this as late as possible, so that most entries end up cached.
1742        """
1743        # TODO: this doesn't update the stat information for directories. So
1744        #       the first 'bzr status' will still need to rewrite
1745        #       .bzr/checkout/dirstate. However, we at least don't need to
1746        #       re-read all of the files.
1747        # TODO: If the operation took a while, we could do a time.sleep(3) here
1748        #       to allow the clock to tick over and ensure we won't have any
1749        #       problems. (we could observe start time, and finish time, and if
1750        #       it is less than eg 10% overhead, add a sleep call.)
1751        paths = FinalPaths(self)
1752        for trans_id, observed in self._observed_sha1s.items():
1753            path = paths.get_path(trans_id)
1754            self._tree._observed_sha1(path, observed)
1755
1756    def get_preview_tree(self):
1757        """Return a tree representing the result of the transform.
1758
1759        The tree is a snapshot, and altering the TreeTransform will invalidate
1760        it.
1761        """
1762        return InventoryPreviewTree(self)
1763
1764    def _inventory_altered(self):
1765        """Determine which trans_ids need new Inventory entries.
1766
1767        An new entry is needed when anything that would be reflected by an
1768        inventory entry changes, including file name, file_id, parent file_id,
1769        file kind, and the execute bit.
1770
1771        Some care is taken to return entries with real changes, not cases
1772        where the value is deleted and then restored to its original value,
1773        but some actually unchanged values may be returned.
1774
1775        :returns: A list of (path, trans_id) for all items requiring an
1776            inventory change. Ordered by path.
1777        """
1778        changed_ids = set()
1779        # Find entries whose file_ids are new (or changed).
1780        new_file_id = set(t for t in self._new_id
1781                          if self._new_id[t] != self.tree_file_id(t))
1782        for id_set in [self._new_name, self._new_parent, new_file_id,
1783                       self._new_executability]:
1784            changed_ids.update(id_set)
1785        # removing implies a kind change
1786        changed_kind = set(self._removed_contents)
1787        # so does adding
1788        changed_kind.intersection_update(self._new_contents)
1789        # Ignore entries that are already known to have changed.
1790        changed_kind.difference_update(changed_ids)
1791        #  to keep only the truly changed ones
1792        changed_kind = (t for t in changed_kind
1793                        if self.tree_kind(t) != self.final_kind(t))
1794        # all kind changes will alter the inventory
1795        changed_ids.update(changed_kind)
1796        # To find entries with changed parent_ids, find parents which existed,
1797        # but changed file_id.
1798        # Now add all their children to the set.
1799        for parent_trans_id in new_file_id:
1800            changed_ids.update(self.iter_tree_children(parent_trans_id))
1801        return sorted(FinalPaths(self).get_paths(changed_ids))
1802
1803    def _generate_inventory_delta(self):
1804        """Generate an inventory delta for the current transform."""
1805        inventory_delta = []
1806        new_paths = self._inventory_altered()
1807        total_entries = len(new_paths) + len(self._removed_id)
1808        with ui.ui_factory.nested_progress_bar() as child_pb:
1809            for num, trans_id in enumerate(self._removed_id):
1810                if (num % 10) == 0:
1811                    child_pb.update(gettext('removing file'),
1812                                    num, total_entries)
1813                if trans_id == self._new_root:
1814                    file_id = self._tree.path2id('')
1815                else:
1816                    file_id = self.tree_file_id(trans_id)
1817                # File-id isn't really being deleted, just moved
1818                if file_id in self._r_new_id:
1819                    continue
1820                path = self._tree_id_paths[trans_id]
1821                inventory_delta.append((path, None, file_id, None))
1822            new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1823                                     new_paths)
1824            for num, (path, trans_id) in enumerate(new_paths):
1825                if (num % 10) == 0:
1826                    child_pb.update(gettext('adding file'),
1827                                    num + len(self._removed_id), total_entries)
1828                file_id = new_path_file_ids[trans_id]
1829                if file_id is None:
1830                    continue
1831                kind = self.final_kind(trans_id)
1832                if kind is None:
1833                    kind = self._tree.stored_kind(self._tree.id2path(file_id))
1834                parent_trans_id = self.final_parent(trans_id)
1835                parent_file_id = new_path_file_ids.get(parent_trans_id)
1836                if parent_file_id is None:
1837                    parent_file_id = self.final_file_id(parent_trans_id)
1838                if trans_id in self._new_reference_revision:
1839                    new_entry = inventory.TreeReference(
1840                        file_id,
1841                        self._new_name[trans_id],
1842                        self.final_file_id(self._new_parent[trans_id]),
1843                        None, self._new_reference_revision[trans_id])
1844                else:
1845                    new_entry = inventory.make_entry(kind,
1846                                                     self.final_name(trans_id),
1847                                                     parent_file_id, file_id)
1848                try:
1849                    old_path = self._tree.id2path(new_entry.file_id)
1850                except errors.NoSuchId:
1851                    old_path = None
1852                new_executability = self._new_executability.get(trans_id)
1853                if new_executability is not None:
1854                    new_entry.executable = new_executability
1855                inventory_delta.append(
1856                    (old_path, path, new_entry.file_id, new_entry))
1857        return inventory_delta
1858
1859
1860class TransformPreview(InventoryTreeTransform):
1861    """A TreeTransform for generating preview trees.
1862
1863    Unlike TreeTransform, this version works when the input tree is a
1864    RevisionTree, rather than a WorkingTree.  As a result, it tends to ignore
1865    unversioned files in the input tree.
1866    """
1867
1868    def __init__(self, tree, pb=None, case_sensitive=True):
1869        tree.lock_read()
1870        limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1871        DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1872
1873    def canonical_path(self, path):
1874        return path
1875
1876    def tree_kind(self, trans_id):
1877        path = self._tree_id_paths.get(trans_id)
1878        if path is None:
1879            return None
1880        kind = self._tree.path_content_summary(path)[0]
1881        if kind == 'missing':
1882            kind = None
1883        return kind
1884
1885    def _set_mode(self, trans_id, mode_id, typefunc):
1886        """Set the mode of new file contents.
1887        The mode_id is the existing file to get the mode from (often the same
1888        as trans_id).  The operation is only performed if there's a mode match
1889        according to typefunc.
1890        """
1891        # is it ok to ignore this?  probably
1892        pass
1893
1894    def iter_tree_children(self, parent_id):
1895        """Iterate through the entry's tree children, if any"""
1896        try:
1897            path = self._tree_id_paths[parent_id]
1898        except KeyError:
1899            return
1900        try:
1901            entry = next(self._tree.iter_entries_by_dir(
1902                specific_files=[path]))[1]
1903        except StopIteration:
1904            return
1905        children = getattr(entry, 'children', {})
1906        for child in children:
1907            childpath = joinpath(path, child)
1908            yield self.trans_id_tree_path(childpath)
1909
1910    def new_orphan(self, trans_id, parent_id):
1911        raise NotImplementedError(self.new_orphan)
1912
1913
1914class InventoryPreviewTree(PreviewTree, inventorytree.InventoryTree):
1915    """Partial implementation of Tree to support show_diff_trees"""
1916
1917    def __init__(self, transform):
1918        PreviewTree.__init__(self, transform)
1919        self._final_paths = FinalPaths(transform)
1920        self._iter_changes_cache = {
1921            c.file_id: c for c in self._transform.iter_changes()}
1922
1923    def supports_setting_file_ids(self):
1924        return True
1925
1926    def supports_tree_reference(self):
1927        # TODO(jelmer): Support tree references in PreviewTree.
1928        # return self._transform._tree.supports_tree_reference()
1929        return False
1930
1931    def _content_change(self, file_id):
1932        """Return True if the content of this file changed"""
1933        changes = self._iter_changes_cache.get(file_id)
1934        return (changes is not None and changes.changed_content)
1935
1936    def _get_file_revision(self, path, file_id, vf, tree_revision):
1937        parent_keys = [
1938            (file_id, t.get_file_revision(t.id2path(file_id)))
1939            for t in self._iter_parent_trees()]
1940        vf.add_lines((file_id, tree_revision), parent_keys,
1941                     self.get_file_lines(path))
1942        repo = self._get_repository()
1943        base_vf = repo.texts
1944        if base_vf not in vf.fallback_versionedfiles:
1945            vf.fallback_versionedfiles.append(base_vf)
1946        return tree_revision
1947
1948    def _stat_limbo_file(self, trans_id):
1949        name = self._transform._limbo_name(trans_id)
1950        return os.lstat(name)
1951
1952    def _comparison_data(self, entry, path):
1953        kind, size, executable, link_or_sha1 = self.path_content_summary(path)
1954        if kind == 'missing':
1955            kind = None
1956            executable = False
1957        else:
1958            file_id = self._transform.final_file_id(self._path2trans_id(path))
1959            executable = self.is_executable(path)
1960        return kind, executable, None
1961
1962    @property
1963    def root_inventory(self):
1964        """This Tree does not use inventory as its backing data."""
1965        raise NotImplementedError(PreviewTree.root_inventory)
1966
1967    def all_file_ids(self):
1968        tree_ids = set(self._transform._tree.all_file_ids())
1969        tree_ids.difference_update(self._transform.tree_file_id(t)
1970                                   for t in self._transform._removed_id)
1971        tree_ids.update(self._transform._new_id.values())
1972        return tree_ids
1973
1974    def all_versioned_paths(self):
1975        tree_paths = set(self._transform._tree.all_versioned_paths())
1976
1977        tree_paths.difference_update(
1978            self._transform.trans_id_tree_path(t)
1979            for t in self._transform._removed_id)
1980
1981        tree_paths.update(
1982            self._final_paths._determine_path(t)
1983            for t in self._transform._new_id)
1984
1985        return tree_paths
1986
1987    def path2id(self, path):
1988        if isinstance(path, list):
1989            if path == []:
1990                path = [""]
1991            path = osutils.pathjoin(*path)
1992        return self._transform.final_file_id(self._path2trans_id(path))
1993
1994    def id2path(self, file_id, recurse='down'):
1995        trans_id = self._transform.trans_id_file_id(file_id)
1996        try:
1997            return self._final_paths._determine_path(trans_id)
1998        except NoFinalPath:
1999            raise errors.NoSuchId(self, file_id)
2000
2001    def extras(self):
2002        possible_extras = set(self._transform.trans_id_tree_path(p) for p
2003                              in self._transform._tree.extras())
2004        possible_extras.update(self._transform._new_contents)
2005        possible_extras.update(self._transform._removed_id)
2006        for trans_id in possible_extras:
2007            if self._transform.final_file_id(trans_id) is None:
2008                yield self._final_paths._determine_path(trans_id)
2009
2010    def _make_inv_entries(self, ordered_entries, specific_files=None):
2011        for trans_id, parent_file_id in ordered_entries:
2012            file_id = self._transform.final_file_id(trans_id)
2013            if file_id is None:
2014                continue
2015            if (specific_files is not None
2016                    and self._final_paths.get_path(trans_id) not in specific_files):
2017                continue
2018            kind = self._transform.final_kind(trans_id)
2019            if kind is None:
2020                kind = self._transform._tree.stored_kind(
2021                    self._transform._tree.id2path(file_id))
2022            new_entry = inventory.make_entry(
2023                kind,
2024                self._transform.final_name(trans_id),
2025                parent_file_id, file_id)
2026            yield new_entry, trans_id
2027
2028    def _list_files_by_dir(self):
2029        todo = [ROOT_PARENT]
2030        ordered_ids = []
2031        while len(todo) > 0:
2032            parent = todo.pop()
2033            parent_file_id = self._transform.final_file_id(parent)
2034            children = list(self._all_children(parent))
2035            paths = dict(zip(children, self._final_paths.get_paths(children)))
2036            children.sort(key=paths.get)
2037            todo.extend(reversed(children))
2038            for trans_id in children:
2039                ordered_ids.append((trans_id, parent_file_id))
2040        return ordered_ids
2041
2042    def iter_child_entries(self, path):
2043        trans_id = self._path2trans_id(path)
2044        if trans_id is None:
2045            raise errors.NoSuchFile(path)
2046        todo = [(child_trans_id, trans_id) for child_trans_id in
2047                self._all_children(trans_id)]
2048        for entry, trans_id in self._make_inv_entries(todo):
2049            yield entry
2050
2051    def iter_entries_by_dir(self, specific_files=None, recurse_nested=False):
2052        if recurse_nested:
2053            raise NotImplementedError(
2054                'follow tree references not yet supported')
2055
2056        # This may not be a maximally efficient implementation, but it is
2057        # reasonably straightforward.  An implementation that grafts the
2058        # TreeTransform changes onto the tree's iter_entries_by_dir results
2059        # might be more efficient, but requires tricky inferences about stack
2060        # position.
2061        ordered_ids = self._list_files_by_dir()
2062        for entry, trans_id in self._make_inv_entries(ordered_ids,
2063                                                      specific_files):
2064            yield self._final_paths.get_path(trans_id), entry
2065
2066    def _iter_entries_for_dir(self, dir_path):
2067        """Return path, entry for items in a directory without recursing down."""
2068        ordered_ids = []
2069        dir_trans_id = self._path2trans_id(dir_path)
2070        dir_id = self._transform.final_file_id(dir_trans_id)
2071        for child_trans_id in self._all_children(dir_trans_id):
2072            ordered_ids.append((child_trans_id, dir_id))
2073        path_entries = []
2074        for entry, trans_id in self._make_inv_entries(ordered_ids):
2075            path_entries.append((self._final_paths.get_path(trans_id), entry))
2076        path_entries.sort()
2077        return path_entries
2078
2079    def list_files(self, include_root=False, from_dir=None, recursive=True,
2080                   recurse_nested=False):
2081        """See WorkingTree.list_files."""
2082        if recurse_nested:
2083            raise NotImplementedError(
2084                'follow tree references not yet supported')
2085
2086        # XXX This should behave like WorkingTree.list_files, but is really
2087        # more like RevisionTree.list_files.
2088        if from_dir == '.':
2089            from_dir = None
2090        if recursive:
2091            prefix = None
2092            if from_dir:
2093                prefix = from_dir + '/'
2094            entries = self.iter_entries_by_dir()
2095            for path, entry in entries:
2096                if entry.name == '' and not include_root:
2097                    continue
2098                if prefix:
2099                    if not path.startswith(prefix):
2100                        continue
2101                    path = path[len(prefix):]
2102                yield path, 'V', entry.kind, entry
2103        else:
2104            if from_dir is None and include_root is True:
2105                root_entry = inventory.make_entry(
2106                    'directory', '', ROOT_PARENT, self.path2id(''))
2107                yield '', 'V', 'directory', root_entry
2108            entries = self._iter_entries_for_dir(from_dir or '')
2109            for path, entry in entries:
2110                yield path, 'V', entry.kind, entry
2111
2112    def get_file_mtime(self, path):
2113        """See Tree.get_file_mtime"""
2114        file_id = self.path2id(path)
2115        if file_id is None:
2116            raise errors.NoSuchFile(path)
2117        if not self._content_change(file_id):
2118            return self._transform._tree.get_file_mtime(
2119                self._transform._tree.id2path(file_id))
2120        trans_id = self._path2trans_id(path)
2121        return self._stat_limbo_file(trans_id).st_mtime
2122
2123    def path_content_summary(self, path):
2124        trans_id = self._path2trans_id(path)
2125        tt = self._transform
2126        tree_path = tt._tree_id_paths.get(trans_id)
2127        kind = tt._new_contents.get(trans_id)
2128        if kind is None:
2129            if tree_path is None or trans_id in tt._removed_contents:
2130                return 'missing', None, None, None
2131            summary = tt._tree.path_content_summary(tree_path)
2132            kind, size, executable, link_or_sha1 = summary
2133        else:
2134            link_or_sha1 = None
2135            limbo_name = tt._limbo_name(trans_id)
2136            if trans_id in tt._new_reference_revision:
2137                kind = 'tree-reference'
2138                link_or_sha1 = tt._new_reference_revision
2139            if kind == 'file':
2140                statval = os.lstat(limbo_name)
2141                size = statval.st_size
2142                if not tt._limbo_supports_executable():
2143                    executable = False
2144                else:
2145                    executable = statval.st_mode & S_IEXEC
2146            else:
2147                size = None
2148                executable = None
2149            if kind == 'symlink':
2150                link_or_sha1 = os.readlink(limbo_name)
2151                if not isinstance(link_or_sha1, str):
2152                    link_or_sha1 = link_or_sha1.decode(osutils._fs_enc)
2153        executable = tt._new_executability.get(trans_id, executable)
2154        return kind, size, executable, link_or_sha1
2155
2156    def iter_changes(self, from_tree, include_unchanged=False,
2157                     specific_files=None, pb=None, extra_trees=None,
2158                     require_versioned=True, want_unversioned=False):
2159        """See InterTree.iter_changes.
2160
2161        This has a fast path that is only used when the from_tree matches
2162        the transform tree, and no fancy options are supplied.
2163        """
2164        if (from_tree is not self._transform._tree or include_unchanged
2165                or specific_files or want_unversioned):
2166            return tree.InterTree.get(from_tree, self).iter_changes(
2167                include_unchanged=include_unchanged,
2168                specific_files=specific_files,
2169                pb=pb,
2170                extra_trees=extra_trees,
2171                require_versioned=require_versioned,
2172                want_unversioned=want_unversioned)
2173        if want_unversioned:
2174            raise ValueError('want_unversioned is not supported')
2175        return self._transform.iter_changes()
2176
2177    def annotate_iter(self, path,
2178                      default_revision=_mod_revision.CURRENT_REVISION):
2179        file_id = self.path2id(path)
2180        changes = self._iter_changes_cache.get(file_id)
2181        if changes is None:
2182            if file_id is None:
2183                old_path = None
2184            else:
2185                old_path = self._transform._tree.id2path(file_id)
2186        else:
2187            if changes.kind[1] is None:
2188                return None
2189            if changes.kind[0] == 'file' and changes.versioned[0]:
2190                old_path = changes.path[0]
2191            else:
2192                old_path = None
2193        if old_path is not None:
2194            old_annotation = self._transform._tree.annotate_iter(
2195                old_path, default_revision=default_revision)
2196        else:
2197            old_annotation = []
2198        if changes is None:
2199            if old_path is None:
2200                return None
2201            else:
2202                return old_annotation
2203        if not changes.changed_content:
2204            return old_annotation
2205        # TODO: This is doing something similar to what WT.annotate_iter is
2206        #       doing, however it fails slightly because it doesn't know what
2207        #       the *other* revision_id is, so it doesn't know how to give the
2208        #       other as the origin for some lines, they all get
2209        #       'default_revision'
2210        #       It would be nice to be able to use the new Annotator based
2211        #       approach, as well.
2212        return annotate.reannotate([old_annotation],
2213                                   self.get_file_lines(path),
2214                                   default_revision)
2215
2216    def walkdirs(self, prefix=''):
2217        pending = [self._transform.root]
2218        while len(pending) > 0:
2219            parent_id = pending.pop()
2220            children = []
2221            subdirs = []
2222            prefix = prefix.rstrip('/')
2223            parent_path = self._final_paths.get_path(parent_id)
2224            parent_file_id = self._transform.final_file_id(parent_id)
2225            for child_id in self._all_children(parent_id):
2226                path_from_root = self._final_paths.get_path(child_id)
2227                basename = self._transform.final_name(child_id)
2228                file_id = self._transform.final_file_id(child_id)
2229                kind = self._transform.final_kind(child_id)
2230                if kind is not None:
2231                    versioned_kind = kind
2232                else:
2233                    kind = 'unknown'
2234                    versioned_kind = self._transform._tree.stored_kind(
2235                        path_from_root)
2236                if versioned_kind == 'directory':
2237                    subdirs.append(child_id)
2238                children.append((path_from_root, basename, kind, None,
2239                                 versioned_kind))
2240            children.sort()
2241            if parent_path.startswith(prefix):
2242                yield parent_path, children
2243            pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2244                                  reverse=True))
2245
2246    def get_symlink_target(self, path):
2247        """See Tree.get_symlink_target"""
2248        file_id = self.path2id(path)
2249        if not self._content_change(file_id):
2250            return self._transform._tree.get_symlink_target(path)
2251        trans_id = self._path2trans_id(path)
2252        name = self._transform._limbo_name(trans_id)
2253        return osutils.readlink(name)
2254
2255    def get_file(self, path):
2256        """See Tree.get_file"""
2257        file_id = self.path2id(path)
2258        if not self._content_change(file_id):
2259            return self._transform._tree.get_file(path)
2260        trans_id = self._path2trans_id(path)
2261        name = self._transform._limbo_name(trans_id)
2262        return open(name, 'rb')
2263
2264
2265def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
2266               delta_from_tree=False):
2267    """Create working tree for a branch, using a TreeTransform.
2268
2269    This function should be used on empty trees, having a tree root at most.
2270    (see merge and revert functionality for working with existing trees)
2271
2272    Existing files are handled like so:
2273
2274    - Existing bzrdirs take precedence over creating new items.  They are
2275      created as '%s.diverted' % name.
2276    - Otherwise, if the content on disk matches the content we are building,
2277      it is silently replaced.
2278    - Otherwise, conflict resolution will move the old file to 'oldname.moved'.
2279
2280    :param tree: The tree to convert wt into a copy of
2281    :param wt: The working tree that files will be placed into
2282    :param accelerator_tree: A tree which can be used for retrieving file
2283        contents more quickly than tree itself, i.e. a workingtree.  tree
2284        will be used for cases where accelerator_tree's content is different.
2285    :param hardlink: If true, hard-link files to accelerator_tree, where
2286        possible.  accelerator_tree must implement abspath, i.e. be a
2287        working tree.
2288    :param delta_from_tree: If true, build_tree may use the input Tree to
2289        generate the inventory delta.
2290    """
2291    with contextlib.ExitStack() as exit_stack:
2292        exit_stack.enter_context(wt.lock_tree_write())
2293        exit_stack.enter_context(tree.lock_read())
2294        if accelerator_tree is not None:
2295            exit_stack.enter_context(accelerator_tree.lock_read())
2296        return _build_tree(tree, wt, accelerator_tree, hardlink,
2297                           delta_from_tree)
2298
2299
2300def resolve_checkout(tt, conflicts, divert):
2301    new_conflicts = set()
2302    for c_type, conflict in ((c[0], c) for c in conflicts):
2303        # Anything but a 'duplicate' would indicate programmer error
2304        if c_type != 'duplicate':
2305            raise AssertionError(c_type)
2306        # Now figure out which is new and which is old
2307        if tt.new_contents(conflict[1]):
2308            new_file = conflict[1]
2309            old_file = conflict[2]
2310        else:
2311            new_file = conflict[2]
2312            old_file = conflict[1]
2313
2314        # We should only get here if the conflict wasn't completely
2315        # resolved
2316        final_parent = tt.final_parent(old_file)
2317        if new_file in divert:
2318            new_name = tt.final_name(old_file) + '.diverted'
2319            tt.adjust_path(new_name, final_parent, new_file)
2320            new_conflicts.add((c_type, 'Diverted to',
2321                               new_file, old_file))
2322        else:
2323            new_name = tt.final_name(old_file) + '.moved'
2324            tt.adjust_path(new_name, final_parent, old_file)
2325            new_conflicts.add((c_type, 'Moved existing file to',
2326                               old_file, new_file))
2327    return new_conflicts
2328
2329
2330def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
2331    """See build_tree."""
2332    for num, _unused in enumerate(wt.all_versioned_paths()):
2333        if num > 0:  # more than just a root
2334            raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
2335    file_trans_id = {}
2336    top_pb = ui.ui_factory.nested_progress_bar()
2337    pp = ProgressPhase("Build phase", 2, top_pb)
2338    if tree.path2id('') is not None:
2339        # This is kind of a hack: we should be altering the root
2340        # as part of the regular tree shape diff logic.
2341        # The conditional test here is to avoid doing an
2342        # expensive operation (flush) every time the root id
2343        # is set within the tree, nor setting the root and thus
2344        # marking the tree as dirty, because we use two different
2345        # idioms here: tree interfaces and inventory interfaces.
2346        if wt.path2id('') != tree.path2id(''):
2347            wt.set_root_id(tree.path2id(''))
2348            wt.flush()
2349    tt = wt.transform()
2350    divert = set()
2351    try:
2352        pp.next_phase()
2353        file_trans_id[find_previous_path(wt, tree, '')] = tt.trans_id_tree_path('')
2354        with ui.ui_factory.nested_progress_bar() as pb:
2355            deferred_contents = []
2356            num = 0
2357            total = len(tree.all_versioned_paths())
2358            if delta_from_tree:
2359                precomputed_delta = []
2360            else:
2361                precomputed_delta = None
2362            # Check if tree inventory has content. If so, we populate
2363            # existing_files with the directory content. If there are no
2364            # entries we skip populating existing_files as its not used.
2365            # This improves performance and unncessary work on large
2366            # directory trees. (#501307)
2367            if total > 0:
2368                existing_files = set()
2369                for dir, files in wt.walkdirs():
2370                    existing_files.update(f[0] for f in files)
2371            for num, (tree_path, entry) in \
2372                    enumerate(tree.iter_entries_by_dir()):
2373                pb.update(gettext("Building tree"), num
2374                          - len(deferred_contents), total)
2375                if entry.parent_id is None:
2376                    continue
2377                reparent = False
2378                file_id = entry.file_id
2379                if delta_from_tree:
2380                    precomputed_delta.append((None, tree_path, file_id, entry))
2381                if tree_path in existing_files:
2382                    target_path = wt.abspath(tree_path)
2383                    kind = osutils.file_kind(target_path)
2384                    if kind == "directory":
2385                        try:
2386                            controldir.ControlDir.open(target_path)
2387                        except errors.NotBranchError:
2388                            pass
2389                        else:
2390                            divert.add(tree_path)
2391                    if (tree_path not in divert
2392                        and _content_match(
2393                            tree, entry, tree_path, kind, target_path)):
2394                        tt.delete_contents(tt.trans_id_tree_path(tree_path))
2395                        if kind == 'directory':
2396                            reparent = True
2397                parent_id = file_trans_id[osutils.dirname(tree_path)]
2398                if entry.kind == 'file':
2399                    # We *almost* replicate new_by_entry, so that we can defer
2400                    # getting the file text, and get them all at once.
2401                    trans_id = tt.create_path(entry.name, parent_id)
2402                    file_trans_id[tree_path] = trans_id
2403                    tt.version_file(trans_id, file_id=file_id)
2404                    executable = tree.is_executable(tree_path)
2405                    if executable:
2406                        tt.set_executability(executable, trans_id)
2407                    trans_data = (trans_id, tree_path, entry.text_sha1)
2408                    deferred_contents.append((tree_path, trans_data))
2409                else:
2410                    file_trans_id[tree_path] = new_by_entry(
2411                        tree_path, tt, entry, parent_id, tree)
2412                if reparent:
2413                    new_trans_id = file_trans_id[tree_path]
2414                    old_parent = tt.trans_id_tree_path(tree_path)
2415                    _reparent_children(tt, old_parent, new_trans_id)
2416            offset = num + 1 - len(deferred_contents)
2417            _create_files(tt, tree, deferred_contents, pb, offset,
2418                          accelerator_tree, hardlink)
2419        pp.next_phase()
2420        divert_trans = set(file_trans_id[f] for f in divert)
2421
2422        def resolver(t, c):
2423            return resolve_checkout(t, c, divert_trans)
2424        raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
2425        if len(raw_conflicts) > 0:
2426            precomputed_delta = None
2427        conflicts = tt.cook_conflicts(raw_conflicts)
2428        for conflict in conflicts:
2429            trace.warning(str(conflict))
2430        try:
2431            wt.add_conflicts(conflicts)
2432        except errors.UnsupportedOperation:
2433            pass
2434        result = tt.apply(no_conflicts=True,
2435                          precomputed_delta=precomputed_delta)
2436    finally:
2437        tt.finalize()
2438        top_pb.finished()
2439    return result
2440
2441
2442def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
2443                  hardlink):
2444    total = len(desired_files) + offset
2445    wt = tt._tree
2446    if accelerator_tree is None:
2447        new_desired_files = desired_files
2448    else:
2449        iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
2450        unchanged = [
2451            change.path for change in iter
2452            if not (change.changed_content or change.executable[0] != change.executable[1])]
2453        if accelerator_tree.supports_content_filtering():
2454            unchanged = [(tp, ap) for (tp, ap) in unchanged
2455                         if not next(accelerator_tree.iter_search_rules([ap]))]
2456        unchanged = dict(unchanged)
2457        new_desired_files = []
2458        count = 0
2459        for unused_tree_path, (trans_id, tree_path, text_sha1) in desired_files:
2460            accelerator_path = unchanged.get(tree_path)
2461            if accelerator_path is None:
2462                new_desired_files.append((tree_path,
2463                                          (trans_id, tree_path, text_sha1)))
2464                continue
2465            pb.update(gettext('Adding file contents'), count + offset, total)
2466            if hardlink:
2467                tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
2468                                   trans_id)
2469            else:
2470                with accelerator_tree.get_file(accelerator_path) as f:
2471                    chunks = osutils.file_iterator(f)
2472                    if wt.supports_content_filtering():
2473                        filters = wt._content_filter_stack(tree_path)
2474                        chunks = filtered_output_bytes(chunks, filters,
2475                                                       ContentFilterContext(tree_path, tree))
2476                    tt.create_file(chunks, trans_id, sha1=text_sha1)
2477            count += 1
2478        offset += count
2479    for count, ((trans_id, tree_path, text_sha1), contents) in enumerate(
2480            tree.iter_files_bytes(new_desired_files)):
2481        if wt.supports_content_filtering():
2482            filters = wt._content_filter_stack(tree_path)
2483            contents = filtered_output_bytes(contents, filters,
2484                                             ContentFilterContext(tree_path, tree))
2485        tt.create_file(contents, trans_id, sha1=text_sha1)
2486        pb.update(gettext('Adding file contents'), count + offset, total)
2487
2488
2489
2490