~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transform.py

  • Committer: Aaron Bentley
  • Date: 2007-02-06 14:52:16 UTC
  • mfrom: (2266 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2268.
  • Revision ID: abentley@panoramicfeedback.com-20070206145216-fcpi8o3ufvuzwbp9
Merge bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006, 2007, 2008 Canonical Ltd
 
1
# Copyright (C) 2006 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
16
16
 
17
17
import os
18
18
import errno
19
 
from stat import S_ISREG, S_IEXEC
20
 
import tempfile
 
19
from stat import S_ISREG
21
20
 
22
 
from bzrlib.lazy_import import lazy_import
23
 
lazy_import(globals(), """
24
 
from bzrlib import (
25
 
    annotate,
26
 
    bzrdir,
27
 
    delta,
28
 
    errors,
29
 
    inventory,
30
 
    revision as _mod_revision,
31
 
    )
32
 
""")
 
21
from bzrlib import bzrdir, errors
33
22
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
34
23
                           ReusingTransform, NotVersionedError, CantMoveRoot,
35
 
                           ExistingLimbo, ImmortalLimbo, NoFinalPath,
36
 
                           UnableCreateSymlink)
 
24
                           ExistingLimbo, ImmortalLimbo, NoFinalPath)
37
25
from bzrlib.inventory import InventoryEntry
38
 
from bzrlib.osutils import (
39
 
    delete_any,
40
 
    file_kind,
41
 
    has_symlinks,
42
 
    lexists,
43
 
    pathjoin,
44
 
    splitpath,
45
 
    supports_executable,
46
 
)
 
26
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
 
27
                            delete_any)
47
28
from bzrlib.progress import DummyProgress, ProgressPhase
48
 
from bzrlib.symbol_versioning import (
49
 
        deprecated_function,
50
 
        )
51
29
from bzrlib.trace import mutter, warning
52
30
from bzrlib import tree
53
 
import bzrlib.ui
 
31
import bzrlib.ui 
54
32
import bzrlib.urlutils as urlutils
55
33
 
56
34
 
64
42
 
65
43
 
66
44
class _TransformResults(object):
67
 
    def __init__(self, modified_paths, rename_count):
 
45
    def __init__(self, modified_paths):
68
46
        object.__init__(self)
69
47
        self.modified_paths = modified_paths
70
 
        self.rename_count = rename_count
71
 
 
72
 
 
73
 
class TreeTransformBase(object):
74
 
    """The base class for TreeTransform and TreeTransformBase"""
75
 
 
76
 
    def __init__(self, tree, limbodir, pb=DummyProgress(),
77
 
                 case_sensitive=True):
78
 
        """Constructor.
79
 
 
80
 
        :param tree: The tree that will be transformed, but not necessarily
81
 
            the output tree.
82
 
        :param limbodir: A directory where new files can be stored until
83
 
            they are installed in their proper places
84
 
        :param pb: A ProgressBar indicating how much progress is being made
85
 
        :param case_sensitive: If True, the target of the transform is
86
 
            case sensitive, not just case preserving.
 
48
 
 
49
 
 
50
class TreeTransform(object):
 
51
    """Represent a tree transformation.
 
52
    
 
53
    This object is designed to support incremental generation of the transform,
 
54
    in any order.  
 
55
    
 
56
    It is easy to produce malformed transforms, but they are generally
 
57
    harmless.  Attempting to apply a malformed transform will cause an
 
58
    exception to be raised before any modifications are made to the tree.  
 
59
 
 
60
    Many kinds of malformed transforms can be corrected with the 
 
61
    resolve_conflicts function.  The remaining ones indicate programming error,
 
62
    such as trying to create a file with no path.
 
63
 
 
64
    Two sets of file creation methods are supplied.  Convenience methods are:
 
65
     * new_file
 
66
     * new_directory
 
67
     * new_symlink
 
68
 
 
69
    These are composed of the low-level methods:
 
70
     * create_path
 
71
     * create_file or create_directory or create_symlink
 
72
     * version_file
 
73
     * set_executability
 
74
    """
 
75
    def __init__(self, tree, pb=DummyProgress()):
 
76
        """Note: a tree_write lock is taken on the tree.
 
77
        
 
78
        Use TreeTransform.finalize() to release the lock
87
79
        """
88
80
        object.__init__(self)
89
81
        self._tree = tree
90
 
        self._limbodir = limbodir
91
 
        self._deletiondir = None
 
82
        self._tree.lock_tree_write()
 
83
        try:
 
84
            control_files = self._tree._control_files
 
85
            self._limbodir = urlutils.local_path_from_url(
 
86
                control_files.controlfilename('limbo'))
 
87
            try:
 
88
                os.mkdir(self._limbodir)
 
89
            except OSError, e:
 
90
                if e.errno == errno.EEXIST:
 
91
                    raise ExistingLimbo(self._limbodir)
 
92
        except: 
 
93
            self._tree.unlock()
 
94
            raise
 
95
 
92
96
        self._id_number = 0
93
 
        # mapping of trans_id -> new basename
94
97
        self._new_name = {}
95
 
        # mapping of trans_id -> new parent trans_id
96
98
        self._new_parent = {}
97
 
        # mapping of trans_id with new contents -> new file_kind
98
99
        self._new_contents = {}
99
 
        # A mapping of transform ids to their limbo filename
100
 
        self._limbo_files = {}
101
 
        # A mapping of transform ids to a set of the transform ids of children
102
 
        # that their limbo directory has
103
 
        self._limbo_children = {}
104
 
        # Map transform ids to maps of child filename to child transform id
105
 
        self._limbo_children_names = {}
106
 
        # List of transform ids that need to be renamed from limbo into place
107
 
        self._needs_rename = set()
108
 
        # Set of trans_ids whose contents will be removed
109
100
        self._removed_contents = set()
110
 
        # Mapping of trans_id -> new execute-bit value
111
101
        self._new_executability = {}
112
 
        # Mapping of trans_id -> new tree-reference value
113
 
        self._new_reference_revision = {}
114
 
        # Mapping of trans_id -> new file_id
115
102
        self._new_id = {}
116
 
        # Mapping of old file-id -> trans_id
117
103
        self._non_present_ids = {}
118
 
        # Mapping of new file_id -> trans_id
119
104
        self._r_new_id = {}
120
 
        # Set of file_ids that will be removed
121
105
        self._removed_id = set()
122
 
        # Mapping of path in old tree -> trans_id
123
106
        self._tree_path_ids = {}
124
 
        # Mapping trans_id -> path in old tree
125
107
        self._tree_id_paths = {}
 
108
        self._realpaths = {}
126
109
        # Cache of realpath results, to speed up canonical_path
127
 
        self._realpaths = {}
 
110
        self._relpaths = {}
128
111
        # Cache of relpath results, to speed up canonical_path
129
 
        self._relpaths = {}
130
 
        # The trans_id that will be used as the tree root
131
112
        self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
132
 
        # Indictor of whether the transform has been applied
133
 
        self._done = False
134
 
        # A progress bar
 
113
        self.__done = False
135
114
        self._pb = pb
136
 
        # Whether the target is case sensitive
137
 
        self._case_sensitive_target = case_sensitive
138
 
        # A counter of how many files have been renamed
139
 
        self.rename_count = 0
140
115
 
141
116
    def __get_root(self):
142
117
        return self._new_root
144
119
    root = property(__get_root)
145
120
 
146
121
    def finalize(self):
147
 
        """Release the working tree lock, if held, clean up limbo dir.
148
 
 
149
 
        This is required if apply has not been invoked, but can be invoked
150
 
        even after apply.
151
 
        """
 
122
        """Release the working tree lock, if held, clean up limbo dir."""
152
123
        if self._tree is None:
153
124
            return
154
125
        try:
155
 
            entries = [(self._limbo_name(t), t, k) for t, k in
156
 
                       self._new_contents.iteritems()]
157
 
            entries.sort(reverse=True)
158
 
            for path, trans_id, kind in entries:
 
126
            for trans_id, kind in self._new_contents.iteritems():
 
127
                path = self._limbo_name(trans_id)
159
128
                if kind == "directory":
160
129
                    os.rmdir(path)
161
130
                else:
165
134
            except OSError:
166
135
                # We don't especially care *why* the dir is immortal.
167
136
                raise ImmortalLimbo(self._limbodir)
168
 
            try:
169
 
                if self._deletiondir is not None:
170
 
                    os.rmdir(self._deletiondir)
171
 
            except OSError:
172
 
                raise errors.ImmortalPendingDeletion(self._deletiondir)
173
137
        finally:
174
138
            self._tree.unlock()
175
139
            self._tree = None
191
155
        """Change the path that is assigned to a transaction id."""
192
156
        if trans_id == self._new_root:
193
157
            raise CantMoveRoot
194
 
        previous_parent = self._new_parent.get(trans_id)
195
 
        previous_name = self._new_name.get(trans_id)
196
158
        self._new_name[trans_id] = name
197
159
        self._new_parent[trans_id] = parent
198
 
        if (trans_id in self._limbo_files and
199
 
            trans_id not in self._needs_rename):
200
 
            self._rename_in_limbo([trans_id])
201
 
            self._limbo_children[previous_parent].remove(trans_id)
202
 
            del self._limbo_children_names[previous_parent][previous_name]
203
 
 
204
 
    def _rename_in_limbo(self, trans_ids):
205
 
        """Fix limbo names so that the right final path is produced.
206
 
 
207
 
        This means we outsmarted ourselves-- we tried to avoid renaming
208
 
        these files later by creating them with their final names in their
209
 
        final parents.  But now the previous name or parent is no longer
210
 
        suitable, so we have to rename them.
211
 
 
212
 
        Even for trans_ids that have no new contents, we must remove their
213
 
        entries from _limbo_files, because they are now stale.
214
 
        """
215
 
        for trans_id in trans_ids:
216
 
            old_path = self._limbo_files.pop(trans_id)
217
 
            if trans_id not in self._new_contents:
218
 
                continue
219
 
            new_path = self._limbo_name(trans_id)
220
 
            os.rename(old_path, new_path)
221
160
 
222
161
    def adjust_root_path(self, name, parent):
223
162
        """Emulate moving the root by moving all children, instead.
257
196
        This reflects only files that already exist, not ones that will be
258
197
        added by transactions.
259
198
        """
260
 
        path = self._tree.id2path(inventory_id)
 
199
        path = self._tree.inventory.id2path(inventory_id)
261
200
        return self.trans_id_tree_path(path)
262
201
 
263
202
    def trans_id_file_id(self, file_id):
354
293
        try:
355
294
            mode = os.stat(self._tree.abspath(old_path)).st_mode
356
295
        except OSError, e:
357
 
            if e.errno in (errno.ENOENT, errno.ENOTDIR):
358
 
                # Either old_path doesn't exist, or the parent of the
359
 
                # target is not a directory (but will be one eventually)
360
 
                # Either way, we know it doesn't exist *right now*
361
 
                # See also bug #248448
 
296
            if e.errno == errno.ENOENT:
362
297
                return
363
298
            else:
364
299
                raise
365
300
        if typefunc(mode):
366
301
            os.chmod(self._limbo_name(trans_id), mode)
367
302
 
368
 
    def create_hardlink(self, path, trans_id):
369
 
        """Schedule creation of a hard link"""
370
 
        name = self._limbo_name(trans_id)
371
 
        try:
372
 
            os.link(path, name)
373
 
        except OSError, e:
374
 
            if e.errno != errno.EPERM:
375
 
                raise
376
 
            raise errors.HardLinkNotSupported(path)
377
 
        try:
378
 
            unique_add(self._new_contents, trans_id, 'file')
379
 
        except:
380
 
            # Clean up the file, it never got registered so
381
 
            # TreeTransform.finalize() won't clean it up.
382
 
            os.unlink(name)
383
 
            raise
384
 
 
385
303
    def create_directory(self, trans_id):
386
304
        """Schedule creation of a new directory.
387
305
        
396
314
        target is a bytestring.
397
315
        See also new_symlink.
398
316
        """
399
 
        if has_symlinks():
400
 
            os.symlink(target, self._limbo_name(trans_id))
401
 
            unique_add(self._new_contents, trans_id, 'symlink')
402
 
        else:
403
 
            try:
404
 
                path = FinalPaths(self).get_path(trans_id)
405
 
            except KeyError:
406
 
                path = None
407
 
            raise UnableCreateSymlink(path=path)
 
317
        os.symlink(target, self._limbo_name(trans_id))
 
318
        unique_add(self._new_contents, trans_id, 'symlink')
408
319
 
409
320
    def cancel_creation(self, trans_id):
410
321
        """Cancel the creation of new file contents."""
411
322
        del self._new_contents[trans_id]
412
 
        children = self._limbo_children.get(trans_id)
413
 
        # if this is a limbo directory with children, move them before removing
414
 
        # the directory
415
 
        if children is not None:
416
 
            self._rename_in_limbo(children)
417
 
            del self._limbo_children[trans_id]
418
 
            del self._limbo_children_names[trans_id]
419
323
        delete_any(self._limbo_name(trans_id))
420
324
 
421
325
    def delete_contents(self, trans_id):
445
349
        else:
446
350
            unique_add(self._new_executability, trans_id, executability)
447
351
 
448
 
    def set_tree_reference(self, revision_id, trans_id):
449
 
        """Set the reference associated with a directory"""
450
 
        unique_add(self._new_reference_revision, trans_id, revision_id)
451
 
 
452
352
    def version_file(self, file_id, trans_id):
453
353
        """Schedule a file to become versioned."""
454
 
        if file_id is None:
455
 
            raise ValueError()
 
354
        assert file_id is not None
456
355
        unique_add(self._new_id, trans_id, file_id)
457
356
        unique_add(self._r_new_id, file_id, trans_id)
458
357
 
462
361
        del self._new_id[trans_id]
463
362
        del self._r_new_id[file_id]
464
363
 
465
 
    def new_paths(self, filesystem_only=False):
466
 
        """Determine the paths of all new and changed files.
467
 
 
468
 
        :param filesystem_only: if True, only calculate values for files
469
 
            that require renames or execute bit changes.
470
 
        """
 
364
    def new_paths(self):
 
365
        """Determine the paths of all new and changed files"""
471
366
        new_ids = set()
472
 
        if filesystem_only:
473
 
            id_sets = (self._needs_rename, self._new_executability)
474
 
        else:
475
 
            id_sets = (self._new_name, self._new_parent, self._new_contents,
476
 
                       self._new_id, self._new_executability)
477
 
        for id_set in id_sets:
 
367
        fp = FinalPaths(self)
 
368
        for id_set in (self._new_name, self._new_parent, self._new_contents,
 
369
                       self._new_id, self._new_executability):
478
370
            new_ids.update(id_set)
479
 
        return sorted(FinalPaths(self).get_paths(new_ids))
 
371
        new_paths = [(fp.get_path(t), t) for t in new_ids]
 
372
        new_paths.sort()
 
373
        return new_paths
480
374
 
481
375
    def tree_kind(self, trans_id):
482
376
        """Determine the file kind in the working tree.
517
411
            return None
518
412
        # the file is old; the old id is still valid
519
413
        if self._new_root == trans_id:
520
 
            return self._tree.get_root_id()
 
414
            return self._tree.inventory.root.file_id
521
415
        return self._tree.inventory.path2id(path)
522
416
 
523
417
    def final_file_id(self, trans_id):
527
421
        applied.
528
422
        """
529
423
        try:
 
424
            # there is a new id for this file
 
425
            assert self._new_id[trans_id] is not None
530
426
            return self._new_id[trans_id]
531
427
        except KeyError:
532
428
            if trans_id in self._removed_id:
589
485
 
590
486
    def find_conflicts(self):
591
487
        """Find any violations of inventory or filesystem invariants"""
592
 
        if self._done is True:
 
488
        if self.__done is True:
593
489
            raise ReusingTransform()
594
490
        conflicts = []
595
491
        # ensure all children of all existent parents are known
617
513
                        self.tree_kind(t) == 'directory'])
618
514
        for trans_id in self._removed_id:
619
515
            file_id = self.tree_file_id(trans_id)
620
 
            if file_id is not None:
621
 
                if self._tree.inventory[file_id].kind == 'directory':
622
 
                    parents.append(trans_id)
623
 
            elif self.tree_kind(trans_id) == 'directory':
 
516
            if self._tree.inventory[file_id].kind == 'directory':
624
517
                parents.append(trans_id)
625
518
 
626
519
        for parent_id in parents:
636
529
        try:
637
530
            children = os.listdir(self._tree.abspath(path))
638
531
        except OSError, e:
639
 
            if e.errno not in (errno.ENOENT, errno.ESRCH, errno.ENOTDIR):
 
532
            if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
640
533
                raise
641
534
            return
642
535
            
756
649
    def _duplicate_entries(self, by_parent):
757
650
        """No directory may have two entries with the same name."""
758
651
        conflicts = []
759
 
        if (self._new_name, self._new_parent) == ({}, {}):
760
 
            return conflicts
761
652
        for children in by_parent.itervalues():
762
653
            name_ids = [(self.final_name(t), t) for t in children]
763
 
            if not self._case_sensitive_target:
764
 
                name_ids = [(n.lower(), t) for n, t in name_ids]
765
654
            name_ids.sort()
766
655
            last_name = None
767
656
            last_trans_id = None
785
674
        conflicts = []
786
675
        removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
787
676
                                self._removed_id))
788
 
        all_ids = self._tree.all_file_ids()
789
 
        active_tree_ids = all_ids.difference(removed_tree_ids)
 
677
        active_tree_ids = set((f for f in self._tree.inventory if
 
678
                               f not in removed_tree_ids))
790
679
        for trans_id, file_id in self._new_id.iteritems():
791
680
            if file_id in active_tree_ids:
792
681
                old_trans_id = self.trans_id_tree_file_id(file_id)
825
714
                continue
826
715
            return True
827
716
        return False
 
717
            
 
718
    def apply(self):
 
719
        """Apply all changes to the inventory and filesystem.
 
720
        
 
721
        If filesystem or inventory conflicts are present, MalformedTransform
 
722
        will be thrown.
 
723
        """
 
724
        conflicts = self.find_conflicts()
 
725
        if len(conflicts) != 0:
 
726
            raise MalformedTransform(conflicts=conflicts)
 
727
        limbo_inv = {}
 
728
        inv = self._tree.inventory
 
729
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
730
        try:
 
731
            child_pb.update('Apply phase', 0, 2)
 
732
            self._apply_removals(inv, limbo_inv)
 
733
            child_pb.update('Apply phase', 1, 2)
 
734
            modified_paths = self._apply_insertions(inv, limbo_inv)
 
735
        finally:
 
736
            child_pb.finished()
 
737
        self._tree._write_inventory(inv)
 
738
        self.__done = True
 
739
        self.finalize()
 
740
        return _TransformResults(modified_paths)
828
741
 
829
742
    def _limbo_name(self, trans_id):
830
743
        """Generate the limbo name of a file"""
831
 
        limbo_name = self._limbo_files.get(trans_id)
832
 
        if limbo_name is not None:
833
 
            return limbo_name
834
 
        parent = self._new_parent.get(trans_id)
835
 
        # if the parent directory is already in limbo (e.g. when building a
836
 
        # tree), choose a limbo name inside the parent, to reduce further
837
 
        # renames.
838
 
        use_direct_path = False
839
 
        if self._new_contents.get(parent) == 'directory':
840
 
            filename = self._new_name.get(trans_id)
841
 
            if filename is not None:
842
 
                if parent not in self._limbo_children:
843
 
                    self._limbo_children[parent] = set()
844
 
                    self._limbo_children_names[parent] = {}
845
 
                    use_direct_path = True
846
 
                # the direct path can only be used if no other file has
847
 
                # already taken this pathname, i.e. if the name is unused, or
848
 
                # if it is already associated with this trans_id.
849
 
                elif self._case_sensitive_target:
850
 
                    if (self._limbo_children_names[parent].get(filename)
851
 
                        in (trans_id, None)):
852
 
                        use_direct_path = True
853
 
                else:
854
 
                    for l_filename, l_trans_id in\
855
 
                        self._limbo_children_names[parent].iteritems():
856
 
                        if l_trans_id == trans_id:
857
 
                            continue
858
 
                        if l_filename.lower() == filename.lower():
859
 
                            break
 
744
        return pathjoin(self._limbodir, trans_id)
 
745
 
 
746
    def _apply_removals(self, inv, limbo_inv):
 
747
        """Perform tree operations that remove directory/inventory names.
 
748
        
 
749
        That is, delete files that are to be deleted, and put any files that
 
750
        need renaming into limbo.  This must be done in strict child-to-parent
 
751
        order.
 
752
        """
 
753
        tree_paths = list(self._tree_path_ids.iteritems())
 
754
        tree_paths.sort(reverse=True)
 
755
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
756
        try:
 
757
            for num, data in enumerate(tree_paths):
 
758
                path, trans_id = data
 
759
                child_pb.update('removing file', num, len(tree_paths))
 
760
                full_path = self._tree.abspath(path)
 
761
                if trans_id in self._removed_contents:
 
762
                    delete_any(full_path)
 
763
                elif trans_id in self._new_name or trans_id in \
 
764
                    self._new_parent:
 
765
                    try:
 
766
                        os.rename(full_path, self._limbo_name(trans_id))
 
767
                    except OSError, e:
 
768
                        if e.errno != errno.ENOENT:
 
769
                            raise
 
770
                if trans_id in self._removed_id:
 
771
                    if trans_id == self._new_root:
 
772
                        file_id = self._tree.inventory.root.file_id
860
773
                    else:
861
 
                        use_direct_path = True
862
 
 
863
 
        if use_direct_path:
864
 
            limbo_name = pathjoin(self._limbo_files[parent], filename)
865
 
            self._limbo_children[parent].add(trans_id)
866
 
            self._limbo_children_names[parent][filename] = trans_id
867
 
        else:
868
 
            limbo_name = pathjoin(self._limbodir, trans_id)
869
 
            self._needs_rename.add(trans_id)
870
 
        self._limbo_files[trans_id] = limbo_name
871
 
        return limbo_name
872
 
 
873
 
    def _set_executability(self, path, entry, trans_id):
 
774
                        file_id = self.tree_file_id(trans_id)
 
775
                    del inv[file_id]
 
776
                elif trans_id in self._new_name or trans_id in self._new_parent:
 
777
                    file_id = self.tree_file_id(trans_id)
 
778
                    if file_id is not None:
 
779
                        limbo_inv[trans_id] = inv[file_id]
 
780
                        del inv[file_id]
 
781
        finally:
 
782
            child_pb.finished()
 
783
 
 
784
    def _apply_insertions(self, inv, limbo_inv):
 
785
        """Perform tree operations that insert directory/inventory names.
 
786
        
 
787
        That is, create any files that need to be created, and restore from
 
788
        limbo any files that needed renaming.  This must be done in strict
 
789
        parent-to-child order.
 
790
        """
 
791
        new_paths = self.new_paths()
 
792
        modified_paths = []
 
793
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
794
        try:
 
795
            for num, (path, trans_id) in enumerate(new_paths):
 
796
                child_pb.update('adding file', num, len(new_paths))
 
797
                try:
 
798
                    kind = self._new_contents[trans_id]
 
799
                except KeyError:
 
800
                    kind = contents = None
 
801
                if trans_id in self._new_contents or \
 
802
                    self.path_changed(trans_id):
 
803
                    full_path = self._tree.abspath(path)
 
804
                    try:
 
805
                        os.rename(self._limbo_name(trans_id), full_path)
 
806
                    except OSError, e:
 
807
                        # We may be renaming a dangling inventory id
 
808
                        if e.errno != errno.ENOENT:
 
809
                            raise
 
810
                    if trans_id in self._new_contents:
 
811
                        modified_paths.append(full_path)
 
812
                        del self._new_contents[trans_id]
 
813
 
 
814
                if trans_id in self._new_id:
 
815
                    if kind is None:
 
816
                        kind = file_kind(self._tree.abspath(path))
 
817
                    inv.add_path(path, kind, self._new_id[trans_id])
 
818
                elif trans_id in self._new_name or trans_id in\
 
819
                    self._new_parent:
 
820
                    entry = limbo_inv.get(trans_id)
 
821
                    if entry is not None:
 
822
                        entry.name = self.final_name(trans_id)
 
823
                        parent_path = os.path.dirname(path)
 
824
                        entry.parent_id = \
 
825
                            self._tree.inventory.path2id(parent_path)
 
826
                        inv.add(entry)
 
827
 
 
828
                # requires files and inventory entries to be in place
 
829
                if trans_id in self._new_executability:
 
830
                    self._set_executability(path, inv, trans_id)
 
831
        finally:
 
832
            child_pb.finished()
 
833
        return modified_paths
 
834
 
 
835
    def _set_executability(self, path, inv, trans_id):
874
836
        """Set the executability of versioned files """
 
837
        file_id = inv.path2id(path)
875
838
        new_executability = self._new_executability[trans_id]
876
 
        if entry is not None:
877
 
            entry.executable = new_executability
 
839
        inv[file_id].executable = new_executability
878
840
        if supports_executable():
879
841
            abspath = self._tree.abspath(path)
880
842
            current_mode = os.stat(abspath).st_mode
941
903
        self.create_symlink(target, trans_id)
942
904
        return trans_id
943
905
 
944
 
    def _affected_ids(self):
945
 
        """Return the set of transform ids affected by the transform"""
946
 
        trans_ids = set(self._removed_id)
947
 
        trans_ids.update(self._new_id.keys())
948
 
        trans_ids.update(self._removed_contents)
949
 
        trans_ids.update(self._new_contents.keys())
950
 
        trans_ids.update(self._new_executability.keys())
951
 
        trans_ids.update(self._new_name.keys())
952
 
        trans_ids.update(self._new_parent.keys())
953
 
        return trans_ids
954
 
 
955
 
    def _get_file_id_maps(self):
956
 
        """Return mapping of file_ids to trans_ids in the to and from states"""
957
 
        trans_ids = self._affected_ids()
958
 
        from_trans_ids = {}
959
 
        to_trans_ids = {}
960
 
        # Build up two dicts: trans_ids associated with file ids in the
961
 
        # FROM state, vs the TO state.
962
 
        for trans_id in trans_ids:
963
 
            from_file_id = self.tree_file_id(trans_id)
964
 
            if from_file_id is not None:
965
 
                from_trans_ids[from_file_id] = trans_id
966
 
            to_file_id = self.final_file_id(trans_id)
967
 
            if to_file_id is not None:
968
 
                to_trans_ids[to_file_id] = trans_id
969
 
        return from_trans_ids, to_trans_ids
970
 
 
971
 
    def _from_file_data(self, from_trans_id, from_versioned, file_id):
972
 
        """Get data about a file in the from (tree) state
973
 
 
974
 
        Return a (name, parent, kind, executable) tuple
975
 
        """
976
 
        from_path = self._tree_id_paths.get(from_trans_id)
977
 
        if from_versioned:
978
 
            # get data from working tree if versioned
979
 
            from_entry = self._tree.inventory[file_id]
980
 
            from_name = from_entry.name
981
 
            from_parent = from_entry.parent_id
982
 
        else:
983
 
            from_entry = None
984
 
            if from_path is None:
985
 
                # File does not exist in FROM state
986
 
                from_name = None
987
 
                from_parent = None
988
 
            else:
989
 
                # File exists, but is not versioned.  Have to use path-
990
 
                # splitting stuff
991
 
                from_name = os.path.basename(from_path)
992
 
                tree_parent = self.get_tree_parent(from_trans_id)
993
 
                from_parent = self.tree_file_id(tree_parent)
994
 
        if from_path is not None:
995
 
            from_kind, from_executable, from_stats = \
996
 
                self._tree._comparison_data(from_entry, from_path)
997
 
        else:
998
 
            from_kind = None
999
 
            from_executable = False
1000
 
        return from_name, from_parent, from_kind, from_executable
1001
 
 
1002
 
    def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
1003
 
        """Get data about a file in the to (target) state
1004
 
 
1005
 
        Return a (name, parent, kind, executable) tuple
1006
 
        """
1007
 
        to_name = self.final_name(to_trans_id)
1008
 
        try:
1009
 
            to_kind = self.final_kind(to_trans_id)
1010
 
        except NoSuchFile:
1011
 
            to_kind = None
1012
 
        to_parent = self.final_file_id(self.final_parent(to_trans_id))
1013
 
        if to_trans_id in self._new_executability:
1014
 
            to_executable = self._new_executability[to_trans_id]
1015
 
        elif to_trans_id == from_trans_id:
1016
 
            to_executable = from_executable
1017
 
        else:
1018
 
            to_executable = False
1019
 
        return to_name, to_parent, to_kind, to_executable
1020
 
 
1021
 
    def iter_changes(self):
1022
 
        """Produce output in the same format as Tree.iter_changes.
1023
 
 
1024
 
        Will produce nonsensical results if invoked while inventory/filesystem
1025
 
        conflicts (as reported by TreeTransform.find_conflicts()) are present.
1026
 
 
1027
 
        This reads the Transform, but only reproduces changes involving a
1028
 
        file_id.  Files that are not versioned in either of the FROM or TO
1029
 
        states are not reflected.
1030
 
        """
1031
 
        final_paths = FinalPaths(self)
1032
 
        from_trans_ids, to_trans_ids = self._get_file_id_maps()
1033
 
        results = []
1034
 
        # Now iterate through all active file_ids
1035
 
        for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
1036
 
            modified = False
1037
 
            from_trans_id = from_trans_ids.get(file_id)
1038
 
            # find file ids, and determine versioning state
1039
 
            if from_trans_id is None:
1040
 
                from_versioned = False
1041
 
                from_trans_id = to_trans_ids[file_id]
1042
 
            else:
1043
 
                from_versioned = True
1044
 
            to_trans_id = to_trans_ids.get(file_id)
1045
 
            if to_trans_id is None:
1046
 
                to_versioned = False
1047
 
                to_trans_id = from_trans_id
1048
 
            else:
1049
 
                to_versioned = True
1050
 
 
1051
 
            from_name, from_parent, from_kind, from_executable = \
1052
 
                self._from_file_data(from_trans_id, from_versioned, file_id)
1053
 
 
1054
 
            to_name, to_parent, to_kind, to_executable = \
1055
 
                self._to_file_data(to_trans_id, from_trans_id, from_executable)
1056
 
 
1057
 
            if not from_versioned:
1058
 
                from_path = None
1059
 
            else:
1060
 
                from_path = self._tree_id_paths.get(from_trans_id)
1061
 
            if not to_versioned:
1062
 
                to_path = None
1063
 
            else:
1064
 
                to_path = final_paths.get_path(to_trans_id)
1065
 
            if from_kind != to_kind:
1066
 
                modified = True
1067
 
            elif to_kind in ('file', 'symlink') and (
1068
 
                to_trans_id != from_trans_id or
1069
 
                to_trans_id in self._new_contents):
1070
 
                modified = True
1071
 
            if (not modified and from_versioned == to_versioned and
1072
 
                from_parent==to_parent and from_name == to_name and
1073
 
                from_executable == to_executable):
1074
 
                continue
1075
 
            results.append((file_id, (from_path, to_path), modified,
1076
 
                   (from_versioned, to_versioned),
1077
 
                   (from_parent, to_parent),
1078
 
                   (from_name, to_name),
1079
 
                   (from_kind, to_kind),
1080
 
                   (from_executable, to_executable)))
1081
 
        return iter(sorted(results, key=lambda x:x[1]))
1082
 
 
1083
 
    def get_preview_tree(self):
1084
 
        """Return a tree representing the result of the transform.
1085
 
 
1086
 
        This tree only supports the subset of Tree functionality required
1087
 
        by show_diff_trees.  It must only be compared to tt._tree.
1088
 
        """
1089
 
        return _PreviewTree(self)
1090
 
 
1091
 
 
1092
 
class TreeTransform(TreeTransformBase):
1093
 
    """Represent a tree transformation.
1094
 
 
1095
 
    This object is designed to support incremental generation of the transform,
1096
 
    in any order.
1097
 
 
1098
 
    However, it gives optimum performance when parent directories are created
1099
 
    before their contents.  The transform is then able to put child files
1100
 
    directly in their parent directory, avoiding later renames.
1101
 
 
1102
 
    It is easy to produce malformed transforms, but they are generally
1103
 
    harmless.  Attempting to apply a malformed transform will cause an
1104
 
    exception to be raised before any modifications are made to the tree.
1105
 
 
1106
 
    Many kinds of malformed transforms can be corrected with the
1107
 
    resolve_conflicts function.  The remaining ones indicate programming error,
1108
 
    such as trying to create a file with no path.
1109
 
 
1110
 
    Two sets of file creation methods are supplied.  Convenience methods are:
1111
 
     * new_file
1112
 
     * new_directory
1113
 
     * new_symlink
1114
 
 
1115
 
    These are composed of the low-level methods:
1116
 
     * create_path
1117
 
     * create_file or create_directory or create_symlink
1118
 
     * version_file
1119
 
     * set_executability
1120
 
 
1121
 
    Transform/Transaction ids
1122
 
    -------------------------
1123
 
    trans_ids are temporary ids assigned to all files involved in a transform.
1124
 
    It's possible, even common, that not all files in the Tree have trans_ids.
1125
 
 
1126
 
    trans_ids are used because filenames and file_ids are not good enough
1127
 
    identifiers; filenames change, and not all files have file_ids.  File-ids
1128
 
    are also associated with trans-ids, so that moving a file moves its
1129
 
    file-id.
1130
 
 
1131
 
    trans_ids are only valid for the TreeTransform that generated them.
1132
 
 
1133
 
    Limbo
1134
 
    -----
1135
 
    Limbo is a temporary directory use to hold new versions of files.
1136
 
    Files are added to limbo by create_file, create_directory, create_symlink,
1137
 
    and their convenience variants (new_*).  Files may be removed from limbo
1138
 
    using cancel_creation.  Files are renamed from limbo into their final
1139
 
    location as part of TreeTransform.apply
1140
 
 
1141
 
    Limbo must be cleaned up, by either calling TreeTransform.apply or
1142
 
    calling TreeTransform.finalize.
1143
 
 
1144
 
    Files are placed into limbo inside their parent directories, where
1145
 
    possible.  This reduces subsequent renames, and makes operations involving
1146
 
    lots of files faster.  This optimization is only possible if the parent
1147
 
    directory is created *before* creating any of its children, so avoid
1148
 
    creating children before parents, where possible.
1149
 
 
1150
 
    Pending-deletion
1151
 
    ----------------
1152
 
    This temporary directory is used by _FileMover for storing files that are
1153
 
    about to be deleted.  In case of rollback, the files will be restored.
1154
 
    FileMover does not delete files until it is sure that a rollback will not
1155
 
    happen.
1156
 
    """
1157
 
    def __init__(self, tree, pb=DummyProgress()):
1158
 
        """Note: a tree_write lock is taken on the tree.
1159
 
 
1160
 
        Use TreeTransform.finalize() to release the lock (can be omitted if
1161
 
        TreeTransform.apply() called).
1162
 
        """
1163
 
        tree.lock_tree_write()
1164
 
 
1165
 
        try:
1166
 
            limbodir = urlutils.local_path_from_url(
1167
 
                tree._transport.abspath('limbo'))
1168
 
            try:
1169
 
                os.mkdir(limbodir)
1170
 
            except OSError, e:
1171
 
                if e.errno == errno.EEXIST:
1172
 
                    raise ExistingLimbo(limbodir)
1173
 
            deletiondir = urlutils.local_path_from_url(
1174
 
                tree._transport.abspath('pending-deletion'))
1175
 
            try:
1176
 
                os.mkdir(deletiondir)
1177
 
            except OSError, e:
1178
 
                if e.errno == errno.EEXIST:
1179
 
                    raise errors.ExistingPendingDeletion(deletiondir)
1180
 
        except:
1181
 
            tree.unlock()
1182
 
            raise
1183
 
 
1184
 
        TreeTransformBase.__init__(self, tree, limbodir, pb,
1185
 
                                   tree.case_sensitive)
1186
 
        self._deletiondir = deletiondir
1187
 
 
1188
 
    def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1189
 
        """Apply all changes to the inventory and filesystem.
1190
 
 
1191
 
        If filesystem or inventory conflicts are present, MalformedTransform
1192
 
        will be thrown.
1193
 
 
1194
 
        If apply succeeds, finalize is not necessary.
1195
 
 
1196
 
        :param no_conflicts: if True, the caller guarantees there are no
1197
 
            conflicts, so no check is made.
1198
 
        :param precomputed_delta: An inventory delta to use instead of
1199
 
            calculating one.
1200
 
        :param _mover: Supply an alternate FileMover, for testing
1201
 
        """
1202
 
        if not no_conflicts:
1203
 
            conflicts = self.find_conflicts()
1204
 
            if len(conflicts) != 0:
1205
 
                raise MalformedTransform(conflicts=conflicts)
1206
 
        if precomputed_delta is None:
1207
 
            new_inventory_delta = []
1208
 
            inventory_delta = new_inventory_delta
1209
 
        else:
1210
 
            new_inventory_delta = None
1211
 
            inventory_delta = precomputed_delta
1212
 
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1213
 
        try:
1214
 
            if _mover is None:
1215
 
                mover = _FileMover()
1216
 
            else:
1217
 
                mover = _mover
1218
 
            try:
1219
 
                child_pb.update('Apply phase', 0, 2)
1220
 
                self._apply_removals(new_inventory_delta, mover)
1221
 
                child_pb.update('Apply phase', 1, 2)
1222
 
                modified_paths = self._apply_insertions(new_inventory_delta,
1223
 
                                                        mover)
1224
 
            except:
1225
 
                mover.rollback()
1226
 
                raise
1227
 
            else:
1228
 
                mover.apply_deletions()
1229
 
        finally:
1230
 
            child_pb.finished()
1231
 
        self._tree.apply_inventory_delta(inventory_delta)
1232
 
        self._done = True
1233
 
        self.finalize()
1234
 
        return _TransformResults(modified_paths, self.rename_count)
1235
 
 
1236
 
    def _apply_removals(self, inventory_delta, mover):
1237
 
        """Perform tree operations that remove directory/inventory names.
1238
 
 
1239
 
        That is, delete files that are to be deleted, and put any files that
1240
 
        need renaming into limbo.  This must be done in strict child-to-parent
1241
 
        order.
1242
 
 
1243
 
        If inventory_delta is None, no inventory delta generation is performed.
1244
 
        """
1245
 
        tree_paths = list(self._tree_path_ids.iteritems())
1246
 
        tree_paths.sort(reverse=True)
1247
 
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1248
 
        try:
1249
 
            for num, data in enumerate(tree_paths):
1250
 
                path, trans_id = data
1251
 
                child_pb.update('removing file', num, len(tree_paths))
1252
 
                full_path = self._tree.abspath(path)
1253
 
                if trans_id in self._removed_contents:
1254
 
                    mover.pre_delete(full_path, os.path.join(self._deletiondir,
1255
 
                                     trans_id))
1256
 
                elif trans_id in self._new_name or trans_id in \
1257
 
                    self._new_parent:
1258
 
                    try:
1259
 
                        mover.rename(full_path, self._limbo_name(trans_id))
1260
 
                    except OSError, e:
1261
 
                        if e.errno != errno.ENOENT:
1262
 
                            raise
1263
 
                    else:
1264
 
                        self.rename_count += 1
1265
 
                if (trans_id in self._removed_id
1266
 
                    and inventory_delta is not None):
1267
 
                    if trans_id == self._new_root:
1268
 
                        file_id = self._tree.get_root_id()
1269
 
                    else:
1270
 
                        file_id = self.tree_file_id(trans_id)
1271
 
                    # File-id isn't really being deleted, just moved
1272
 
                    if file_id in self._r_new_id:
1273
 
                        continue
1274
 
                    inventory_delta.append((path, None, file_id, None))
1275
 
        finally:
1276
 
            child_pb.finished()
1277
 
 
1278
 
    def _apply_insertions(self, inventory_delta, mover):
1279
 
        """Perform tree operations that insert directory/inventory names.
1280
 
 
1281
 
        That is, create any files that need to be created, and restore from
1282
 
        limbo any files that needed renaming.  This must be done in strict
1283
 
        parent-to-child order.
1284
 
 
1285
 
        If inventory_delta is None, no inventory delta is calculated, and
1286
 
        no list of modified paths is returned.
1287
 
        """
1288
 
        new_paths = self.new_paths(filesystem_only=(inventory_delta is None))
1289
 
        modified_paths = []
1290
 
        completed_new = []
1291
 
        new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1292
 
                                 new_paths)
1293
 
        if inventory_delta is not None:
1294
 
            entries = self._tree.iter_entries_by_dir(
1295
 
                new_path_file_ids.values())
1296
 
            old_paths = dict((e.file_id, p) for p, e in entries)
1297
 
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1298
 
        try:
1299
 
            for num, (path, trans_id) in enumerate(new_paths):
1300
 
                new_entry = None
1301
 
                if (num % 10) == 0:
1302
 
                    child_pb.update('adding file', num, len(new_paths))
1303
 
                full_path = self._tree.abspath(path)
1304
 
                if trans_id in self._needs_rename:
1305
 
                    try:
1306
 
                        mover.rename(self._limbo_name(trans_id), full_path)
1307
 
                    except OSError, e:
1308
 
                        # We may be renaming a dangling inventory id
1309
 
                        if e.errno != errno.ENOENT:
1310
 
                            raise
1311
 
                    else:
1312
 
                        self.rename_count += 1
1313
 
                if inventory_delta is not None:
1314
 
                    if (trans_id in self._new_contents or
1315
 
                        self.path_changed(trans_id)):
1316
 
                        if trans_id in self._new_contents:
1317
 
                            modified_paths.append(full_path)
1318
 
                            completed_new.append(trans_id)
1319
 
                    file_id = new_path_file_ids[trans_id]
1320
 
                    if file_id is not None and (trans_id in self._new_id or
1321
 
                        trans_id in self._new_name or
1322
 
                        trans_id in self._new_parent
1323
 
                        or trans_id in self._new_executability):
1324
 
                        try:
1325
 
                            kind = self.final_kind(trans_id)
1326
 
                        except NoSuchFile:
1327
 
                            kind = self._tree.stored_kind(file_id)
1328
 
                        parent_trans_id = self.final_parent(trans_id)
1329
 
                        parent_file_id = new_path_file_ids.get(parent_trans_id)
1330
 
                        if parent_file_id is None:
1331
 
                            parent_file_id = self.final_file_id(
1332
 
                                parent_trans_id)
1333
 
                        if trans_id in self._new_reference_revision:
1334
 
                            new_entry = inventory.TreeReference(
1335
 
                                file_id,
1336
 
                                self._new_name[trans_id],
1337
 
                                self.final_file_id(self._new_parent[trans_id]),
1338
 
                                None, self._new_reference_revision[trans_id])
1339
 
                        else:
1340
 
                            new_entry = inventory.make_entry(kind,
1341
 
                                self.final_name(trans_id),
1342
 
                                parent_file_id, file_id)
1343
 
                        old_path = old_paths.get(new_entry.file_id)
1344
 
                        inventory_delta.append(
1345
 
                            (old_path, path, new_entry.file_id, new_entry))
1346
 
 
1347
 
                if trans_id in self._new_executability:
1348
 
                    self._set_executability(path, new_entry, trans_id)
1349
 
        finally:
1350
 
            child_pb.finished()
1351
 
        if inventory_delta is None:
1352
 
            self._new_contents.clear()
1353
 
        else:
1354
 
            for trans_id in completed_new:
1355
 
                del self._new_contents[trans_id]
1356
 
        return modified_paths
1357
 
 
1358
 
 
1359
 
class TransformPreview(TreeTransformBase):
1360
 
    """A TreeTransform for generating preview trees.
1361
 
 
1362
 
    Unlike TreeTransform, this version works when the input tree is a
1363
 
    RevisionTree, rather than a WorkingTree.  As a result, it tends to ignore
1364
 
    unversioned files in the input tree.
1365
 
    """
1366
 
 
1367
 
    def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
1368
 
        tree.lock_read()
1369
 
        limbodir = tempfile.mkdtemp(prefix='bzr-limbo-')
1370
 
        TreeTransformBase.__init__(self, tree, limbodir, pb, case_sensitive)
1371
 
 
1372
 
    def canonical_path(self, path):
1373
 
        return path
1374
 
 
1375
 
    def tree_kind(self, trans_id):
1376
 
        path = self._tree_id_paths.get(trans_id)
1377
 
        if path is None:
1378
 
            raise NoSuchFile(None)
1379
 
        file_id = self._tree.path2id(path)
1380
 
        return self._tree.kind(file_id)
1381
 
 
1382
 
    def _set_mode(self, trans_id, mode_id, typefunc):
1383
 
        """Set the mode of new file contents.
1384
 
        The mode_id is the existing file to get the mode from (often the same
1385
 
        as trans_id).  The operation is only performed if there's a mode match
1386
 
        according to typefunc.
1387
 
        """
1388
 
        # is it ok to ignore this?  probably
1389
 
        pass
1390
 
 
1391
 
    def iter_tree_children(self, parent_id):
1392
 
        """Iterate through the entry's tree children, if any"""
1393
 
        try:
1394
 
            path = self._tree_id_paths[parent_id]
1395
 
        except KeyError:
1396
 
            return
1397
 
        file_id = self.tree_file_id(parent_id)
1398
 
        if file_id is None:
1399
 
            return
1400
 
        children = getattr(self._tree.inventory[file_id], 'children', {})
1401
 
        for child in children:
1402
 
            childpath = joinpath(path, child)
1403
 
            yield self.trans_id_tree_path(childpath)
1404
 
 
1405
 
 
1406
 
class _PreviewTree(tree.Tree):
1407
 
    """Partial implementation of Tree to support show_diff_trees"""
1408
 
 
1409
 
    def __init__(self, transform):
1410
 
        self._transform = transform
1411
 
        self._final_paths = FinalPaths(transform)
1412
 
        self.__by_parent = None
1413
 
        self._parent_ids = []
1414
 
 
1415
 
    def _changes(self, file_id):
1416
 
        for changes in self._transform.iter_changes():
1417
 
            if changes[0] == file_id:
1418
 
                return changes
1419
 
 
1420
 
    def _content_change(self, file_id):
1421
 
        """Return True if the content of this file changed"""
1422
 
        changes = self._changes(file_id)
1423
 
        # changes[2] is true if the file content changed.  See
1424
 
        # InterTree.iter_changes.
1425
 
        return (changes is not None and changes[2])
1426
 
 
1427
 
    def _get_repository(self):
1428
 
        repo = getattr(self._transform._tree, '_repository', None)
1429
 
        if repo is None:
1430
 
            repo = self._transform._tree.branch.repository
1431
 
        return repo
1432
 
 
1433
 
    def _iter_parent_trees(self):
1434
 
        for revision_id in self.get_parent_ids():
1435
 
            try:
1436
 
                yield self.revision_tree(revision_id)
1437
 
            except errors.NoSuchRevisionInTree:
1438
 
                yield self._get_repository().revision_tree(revision_id)
1439
 
 
1440
 
    def _get_file_revision(self, file_id, vf, tree_revision):
1441
 
        parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
1442
 
                       self._iter_parent_trees()]
1443
 
        vf.add_lines((file_id, tree_revision), parent_keys,
1444
 
                     self.get_file(file_id).readlines())
1445
 
        repo = self._get_repository()
1446
 
        base_vf = repo.texts
1447
 
        if base_vf not in vf.fallback_versionedfiles:
1448
 
            vf.fallback_versionedfiles.append(base_vf)
1449
 
        return tree_revision
1450
 
 
1451
 
    def _stat_limbo_file(self, file_id):
1452
 
        trans_id = self._transform.trans_id_file_id(file_id)
1453
 
        name = self._transform._limbo_name(trans_id)
1454
 
        return os.lstat(name)
1455
 
 
1456
 
    @property
1457
 
    def _by_parent(self):
1458
 
        if self.__by_parent is None:
1459
 
            self.__by_parent = self._transform.by_parent()
1460
 
        return self.__by_parent
1461
 
 
1462
 
    def lock_read(self):
1463
 
        # Perhaps in theory, this should lock the TreeTransform?
1464
 
        pass
1465
 
 
1466
 
    def unlock(self):
1467
 
        pass
1468
 
 
1469
 
    @property
1470
 
    def inventory(self):
1471
 
        """This Tree does not use inventory as its backing data."""
1472
 
        raise NotImplementedError(_PreviewTree.inventory)
1473
 
 
1474
 
    def get_root_id(self):
1475
 
        return self._transform.final_file_id(self._transform.root)
1476
 
 
1477
 
    def all_file_ids(self):
1478
 
        tree_ids = set(self._transform._tree.all_file_ids())
1479
 
        tree_ids.difference_update(self._transform.tree_file_id(t)
1480
 
                                   for t in self._transform._removed_id)
1481
 
        tree_ids.update(self._transform._new_id.values())
1482
 
        return tree_ids
1483
 
 
1484
 
    def __iter__(self):
1485
 
        return iter(self.all_file_ids())
1486
 
 
1487
 
    def paths2ids(self, specific_files, trees=None, require_versioned=False):
1488
 
        """See Tree.paths2ids"""
1489
 
        to_find = set(specific_files)
1490
 
        result = set()
1491
 
        for (file_id, paths, changed, versioned, parent, name, kind,
1492
 
             executable) in self._transform.iter_changes():
1493
 
            if paths[1] in to_find:
1494
 
                result.append(file_id)
1495
 
                to_find.remove(paths[1])
1496
 
        result.update(self._transform._tree.paths2ids(to_find,
1497
 
                      trees=[], require_versioned=require_versioned))
1498
 
        return result
1499
 
 
1500
 
    def _path2trans_id(self, path):
1501
 
        segments = splitpath(path)
1502
 
        cur_parent = self._transform.root
1503
 
        for cur_segment in segments:
1504
 
            for child in self._all_children(cur_parent):
1505
 
                if self._transform.final_name(child) == cur_segment:
1506
 
                    cur_parent = child
1507
 
                    break
1508
 
            else:
1509
 
                return None
1510
 
        return cur_parent
1511
 
 
1512
 
    def path2id(self, path):
1513
 
        return self._transform.final_file_id(self._path2trans_id(path))
1514
 
 
1515
 
    def id2path(self, file_id):
1516
 
        trans_id = self._transform.trans_id_file_id(file_id)
1517
 
        try:
1518
 
            return self._final_paths._determine_path(trans_id)
1519
 
        except NoFinalPath:
1520
 
            raise errors.NoSuchId(self, file_id)
1521
 
 
1522
 
    def _all_children(self, trans_id):
1523
 
        children = set(self._transform.iter_tree_children(trans_id))
1524
 
        # children in the _new_parent set are provided by _by_parent.
1525
 
        children.difference_update(self._transform._new_parent.keys())
1526
 
        children.update(self._by_parent.get(trans_id, []))
1527
 
        return children
1528
 
 
1529
 
    def _make_inv_entries(self, ordered_entries, specific_file_ids):
1530
 
        for trans_id, parent_file_id in ordered_entries:
1531
 
            file_id = self._transform.final_file_id(trans_id)
1532
 
            if file_id is None:
1533
 
                continue
1534
 
            if (specific_file_ids is not None
1535
 
                and file_id not in specific_file_ids):
1536
 
                continue
1537
 
            try:
1538
 
                kind = self._transform.final_kind(trans_id)
1539
 
            except NoSuchFile:
1540
 
                kind = self._transform._tree.stored_kind(file_id)
1541
 
            new_entry = inventory.make_entry(
1542
 
                kind,
1543
 
                self._transform.final_name(trans_id),
1544
 
                parent_file_id, file_id)
1545
 
            yield new_entry, trans_id
1546
 
 
1547
 
    def iter_entries_by_dir(self, specific_file_ids=None):
1548
 
        # This may not be a maximally efficient implementation, but it is
1549
 
        # reasonably straightforward.  An implementation that grafts the
1550
 
        # TreeTransform changes onto the tree's iter_entries_by_dir results
1551
 
        # might be more efficient, but requires tricky inferences about stack
1552
 
        # position.
1553
 
        todo = [ROOT_PARENT]
1554
 
        ordered_ids = []
1555
 
        while len(todo) > 0:
1556
 
            parent = todo.pop()
1557
 
            parent_file_id = self._transform.final_file_id(parent)
1558
 
            children = list(self._all_children(parent))
1559
 
            paths = dict(zip(children, self._final_paths.get_paths(children)))
1560
 
            children.sort(key=paths.get)
1561
 
            todo.extend(reversed(children))
1562
 
            for trans_id in children:
1563
 
                ordered_ids.append((trans_id, parent_file_id))
1564
 
        for entry, trans_id in self._make_inv_entries(ordered_ids,
1565
 
                                                      specific_file_ids):
1566
 
            yield unicode(self._final_paths.get_path(trans_id)), entry
1567
 
 
1568
 
    def kind(self, file_id):
1569
 
        trans_id = self._transform.trans_id_file_id(file_id)
1570
 
        return self._transform.final_kind(trans_id)
1571
 
 
1572
 
    def stored_kind(self, file_id):
1573
 
        trans_id = self._transform.trans_id_file_id(file_id)
1574
 
        try:
1575
 
            return self._transform._new_contents[trans_id]
1576
 
        except KeyError:
1577
 
            return self._transform._tree.stored_kind(file_id)
1578
 
 
1579
 
    def get_file_mtime(self, file_id, path=None):
1580
 
        """See Tree.get_file_mtime"""
1581
 
        if not self._content_change(file_id):
1582
 
            return self._transform._tree.get_file_mtime(file_id, path)
1583
 
        return self._stat_limbo_file(file_id).st_mtime
1584
 
 
1585
 
    def get_file_size(self, file_id):
1586
 
        """See Tree.get_file_size"""
1587
 
        if self.kind(file_id) == 'file':
1588
 
            return self._transform._tree.get_file_size(file_id)
1589
 
        else:
1590
 
            return None
1591
 
 
1592
 
    def get_file_sha1(self, file_id, path=None, stat_value=None):
1593
 
        return self._transform._tree.get_file_sha1(file_id)
1594
 
 
1595
 
    def is_executable(self, file_id, path=None):
1596
 
        trans_id = self._transform.trans_id_file_id(file_id)
1597
 
        try:
1598
 
            return self._transform._new_executability[trans_id]
1599
 
        except KeyError:
1600
 
            return self._transform._tree.is_executable(file_id, path)
1601
 
 
1602
 
    def path_content_summary(self, path):
1603
 
        trans_id = self._path2trans_id(path)
1604
 
        tt = self._transform
1605
 
        tree_path = tt._tree_id_paths.get(trans_id)
1606
 
        kind = tt._new_contents.get(trans_id)
1607
 
        if kind is None:
1608
 
            if tree_path is None or trans_id in tt._removed_contents:
1609
 
                return 'missing', None, None, None
1610
 
            summary = tt._tree.path_content_summary(tree_path)
1611
 
            kind, size, executable, link_or_sha1 = summary
1612
 
        else:
1613
 
            link_or_sha1 = None
1614
 
            limbo_name = tt._limbo_name(trans_id)
1615
 
            if trans_id in tt._new_reference_revision:
1616
 
                kind = 'tree-reference'
1617
 
            if kind == 'file':
1618
 
                statval = os.lstat(limbo_name)
1619
 
                size = statval.st_size
1620
 
                if not supports_executable():
1621
 
                    executable = None
1622
 
                else:
1623
 
                    executable = statval.st_mode & S_IEXEC
1624
 
            else:
1625
 
                size = None
1626
 
                executable = None
1627
 
            if kind == 'symlink':
1628
 
                link_or_sha1 = os.readlink(limbo_name)
1629
 
        if supports_executable():
1630
 
            executable = tt._new_executability.get(trans_id, executable)
1631
 
        return kind, size, executable, link_or_sha1
1632
 
 
1633
 
    def iter_changes(self, from_tree, include_unchanged=False,
1634
 
                      specific_files=None, pb=None, extra_trees=None,
1635
 
                      require_versioned=True, want_unversioned=False):
1636
 
        """See InterTree.iter_changes.
1637
 
 
1638
 
        This implementation does not support include_unchanged, specific_files,
1639
 
        or want_unversioned.  extra_trees, require_versioned, and pb are
1640
 
        ignored.
1641
 
        """
1642
 
        if from_tree is not self._transform._tree:
1643
 
            raise ValueError('from_tree must be transform source tree.')
1644
 
        if include_unchanged:
1645
 
            raise ValueError('include_unchanged is not supported')
1646
 
        if specific_files is not None:
1647
 
            raise ValueError('specific_files is not supported')
1648
 
        if want_unversioned:
1649
 
            raise ValueError('want_unversioned is not supported')
1650
 
        return self._transform.iter_changes()
1651
 
 
1652
 
    def get_file(self, file_id, path=None):
1653
 
        """See Tree.get_file"""
1654
 
        if not self._content_change(file_id):
1655
 
            return self._transform._tree.get_file(file_id, path)
1656
 
        trans_id = self._transform.trans_id_file_id(file_id)
1657
 
        name = self._transform._limbo_name(trans_id)
1658
 
        return open(name, 'rb')
1659
 
 
1660
 
    def get_file_text(self, file_id):
1661
 
        text_file = self.get_file(file_id)
1662
 
        try:
1663
 
            return text_file.read()
1664
 
        finally:
1665
 
            text_file.close()
1666
 
 
1667
 
    def annotate_iter(self, file_id,
1668
 
                      default_revision=_mod_revision.CURRENT_REVISION):
1669
 
        changes = self._changes(file_id)
1670
 
        if changes is None:
1671
 
            get_old = True
1672
 
        else:
1673
 
            changed_content, versioned, kind = (changes[2], changes[3],
1674
 
                                                changes[6])
1675
 
            if kind[1] is None:
1676
 
                return None
1677
 
            get_old = (kind[0] == 'file' and versioned[0])
1678
 
        if get_old:
1679
 
            old_annotation = self._transform._tree.annotate_iter(file_id,
1680
 
                default_revision=default_revision)
1681
 
        else:
1682
 
            old_annotation = []
1683
 
        if changes is None:
1684
 
            return old_annotation
1685
 
        if not changed_content:
1686
 
            return old_annotation
1687
 
        return annotate.reannotate([old_annotation],
1688
 
                                   self.get_file(file_id).readlines(),
1689
 
                                   default_revision)
1690
 
 
1691
 
    def get_symlink_target(self, file_id):
1692
 
        """See Tree.get_symlink_target"""
1693
 
        if not self._content_change(file_id):
1694
 
            return self._transform._tree.get_symlink_target(file_id)
1695
 
        trans_id = self._transform.trans_id_file_id(file_id)
1696
 
        name = self._transform._limbo_name(trans_id)
1697
 
        return os.readlink(name)
1698
 
 
1699
 
    def list_files(self, include_root=False):
1700
 
        return self._transform._tree.list_files(include_root)
1701
 
 
1702
 
    def walkdirs(self, prefix=""):
1703
 
        return self._transform._tree.walkdirs(prefix)
1704
 
 
1705
 
    def get_parent_ids(self):
1706
 
        return self._parent_ids
1707
 
 
1708
 
    def set_parent_ids(self, parent_ids):
1709
 
        self._parent_ids = parent_ids
1710
 
 
1711
 
    def get_revision_tree(self, revision_id):
1712
 
        return self._transform._tree.get_revision_tree(revision_id)
1713
 
 
1714
 
 
1715
906
def joinpath(parent, child):
1716
907
    """Join tree-relative paths, handling the tree root specially"""
1717
908
    if parent is None or parent == "":
1747
938
            self._known_paths[trans_id] = self._determine_path(trans_id)
1748
939
        return self._known_paths[trans_id]
1749
940
 
1750
 
    def get_paths(self, trans_ids):
1751
 
        return [(self.get_path(t), t) for t in trans_ids]
1752
 
 
1753
 
 
1754
 
 
1755
941
def topology_sorted_ids(tree):
1756
942
    """Determine the topological order of the ids in a tree"""
1757
943
    file_ids = list(tree)
1759
945
    return file_ids
1760
946
 
1761
947
 
1762
 
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
1763
 
               delta_from_tree=False):
 
948
def build_tree(tree, wt):
1764
949
    """Create working tree for a branch, using a TreeTransform.
1765
950
    
1766
951
    This function should be used on empty trees, having a tree root at most.
1773
958
    - Otherwise, if the content on disk matches the content we are building,
1774
959
      it is silently replaced.
1775
960
    - Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1776
 
 
1777
 
    :param tree: The tree to convert wt into a copy of
1778
 
    :param wt: The working tree that files will be placed into
1779
 
    :param accelerator_tree: A tree which can be used for retrieving file
1780
 
        contents more quickly than tree itself, i.e. a workingtree.  tree
1781
 
        will be used for cases where accelerator_tree's content is different.
1782
 
    :param hardlink: If true, hard-link files to accelerator_tree, where
1783
 
        possible.  accelerator_tree must implement abspath, i.e. be a
1784
 
        working tree.
1785
 
    :param delta_from_tree: If true, build_tree may use the input Tree to
1786
 
        generate the inventory delta.
1787
961
    """
1788
 
    wt.lock_tree_write()
1789
 
    try:
1790
 
        tree.lock_read()
1791
 
        try:
1792
 
            if accelerator_tree is not None:
1793
 
                accelerator_tree.lock_read()
1794
 
            try:
1795
 
                return _build_tree(tree, wt, accelerator_tree, hardlink,
1796
 
                                   delta_from_tree)
1797
 
            finally:
1798
 
                if accelerator_tree is not None:
1799
 
                    accelerator_tree.unlock()
1800
 
        finally:
1801
 
            tree.unlock()
1802
 
    finally:
1803
 
        wt.unlock()
1804
 
 
1805
 
 
1806
 
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
1807
 
    """See build_tree."""
1808
 
    for num, _unused in enumerate(wt.all_file_ids()):
1809
 
        if num > 0:  # more than just a root
1810
 
            raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1811
 
    existing_files = set()
1812
 
    for dir, files in wt.walkdirs():
1813
 
        existing_files.update(f[0] for f in files)
 
962
    if len(wt.inventory) > 1:  # more than just a root
 
963
        raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1814
964
    file_trans_id = {}
1815
965
    top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1816
966
    pp = ProgressPhase("Build phase", 2, top_pb)
1817
967
    if tree.inventory.root is not None:
1818
 
        # This is kind of a hack: we should be altering the root
1819
 
        # as part of the regular tree shape diff logic.
1820
 
        # The conditional test here is to avoid doing an
1821
 
        # expensive operation (flush) every time the root id
1822
 
        # is set within the tree, nor setting the root and thus
1823
 
        # marking the tree as dirty, because we use two different
1824
 
        # idioms here: tree interfaces and inventory interfaces.
1825
 
        if wt.get_root_id() != tree.get_root_id():
1826
 
            wt.set_root_id(tree.get_root_id())
1827
 
            wt.flush()
 
968
        wt.set_root_id(tree.inventory.root.file_id)
1828
969
    tt = TreeTransform(wt)
1829
970
    divert = set()
1830
971
    try:
1833
974
            tt.trans_id_tree_file_id(wt.get_root_id())
1834
975
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
1835
976
        try:
1836
 
            deferred_contents = []
1837
 
            num = 0
1838
 
            total = len(tree.inventory)
1839
 
            if delta_from_tree:
1840
 
                precomputed_delta = []
1841
 
            else:
1842
 
                precomputed_delta = None
1843
977
            for num, (tree_path, entry) in \
1844
978
                enumerate(tree.inventory.iter_entries_by_dir()):
1845
 
                pb.update("Building tree", num - len(deferred_contents), total)
 
979
                pb.update("Building tree", num, len(tree.inventory))
1846
980
                if entry.parent_id is None:
1847
981
                    continue
1848
982
                reparent = False
1849
983
                file_id = entry.file_id
1850
 
                if delta_from_tree:
1851
 
                    precomputed_delta.append((None, tree_path, file_id, entry))
1852
 
                if tree_path in existing_files:
1853
 
                    target_path = wt.abspath(tree_path)
 
984
                target_path = wt.abspath(tree_path)
 
985
                try:
1854
986
                    kind = file_kind(target_path)
 
987
                except NoSuchFile:
 
988
                    pass
 
989
                else:
1855
990
                    if kind == "directory":
1856
991
                        try:
1857
992
                            bzrdir.BzrDir.open(target_path)
1865
1000
                        tt.delete_contents(tt.trans_id_tree_path(tree_path))
1866
1001
                        if kind == 'directory':
1867
1002
                            reparent = True
 
1003
                if entry.parent_id not in file_trans_id:
 
1004
                    raise repr(entry.parent_id)
1868
1005
                parent_id = file_trans_id[entry.parent_id]
1869
 
                if entry.kind == 'file':
1870
 
                    # We *almost* replicate new_by_entry, so that we can defer
1871
 
                    # getting the file text, and get them all at once.
1872
 
                    trans_id = tt.create_path(entry.name, parent_id)
1873
 
                    file_trans_id[file_id] = trans_id
1874
 
                    tt.version_file(file_id, trans_id)
1875
 
                    executable = tree.is_executable(file_id, tree_path)
1876
 
                    if executable:
1877
 
                        tt.set_executability(executable, trans_id)
1878
 
                    deferred_contents.append((file_id, trans_id))
1879
 
                else:
1880
 
                    file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1881
 
                                                          tree)
 
1006
                file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
 
1007
                                                      tree)
1882
1008
                if reparent:
1883
1009
                    new_trans_id = file_trans_id[file_id]
1884
1010
                    old_parent = tt.trans_id_tree_path(tree_path)
1885
1011
                    _reparent_children(tt, old_parent, new_trans_id)
1886
 
            offset = num + 1 - len(deferred_contents)
1887
 
            _create_files(tt, tree, deferred_contents, pb, offset,
1888
 
                          accelerator_tree, hardlink)
1889
1012
        finally:
1890
1013
            pb.finished()
1891
1014
        pp.next_phase()
1892
1015
        divert_trans = set(file_trans_id[f] for f in divert)
1893
1016
        resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1894
1017
        raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1895
 
        if len(raw_conflicts) > 0:
1896
 
            precomputed_delta = None
1897
1018
        conflicts = cook_conflicts(raw_conflicts, tt)
1898
1019
        for conflict in conflicts:
1899
1020
            warning(conflict)
1901
1022
            wt.add_conflicts(conflicts)
1902
1023
        except errors.UnsupportedOperation:
1903
1024
            pass
1904
 
        result = tt.apply(no_conflicts=True,
1905
 
                          precomputed_delta=precomputed_delta)
 
1025
        tt.apply()
1906
1026
    finally:
1907
1027
        tt.finalize()
1908
1028
        top_pb.finished()
1909
 
    return result
1910
 
 
1911
 
 
1912
 
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
1913
 
                  hardlink):
1914
 
    total = len(desired_files) + offset
1915
 
    if accelerator_tree is None:
1916
 
        new_desired_files = desired_files
1917
 
    else:
1918
 
        iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
1919
 
        unchanged = dict((f, p[1]) for (f, p, c, v, d, n, k, e)
1920
 
                         in iter if not (c or e[0] != e[1]))
1921
 
        new_desired_files = []
1922
 
        count = 0
1923
 
        for file_id, trans_id in desired_files:
1924
 
            accelerator_path = unchanged.get(file_id)
1925
 
            if accelerator_path is None:
1926
 
                new_desired_files.append((file_id, trans_id))
1927
 
                continue
1928
 
            pb.update('Adding file contents', count + offset, total)
1929
 
            if hardlink:
1930
 
                tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
1931
 
                                   trans_id)
1932
 
            else:
1933
 
                contents = accelerator_tree.get_file(file_id, accelerator_path)
1934
 
                try:
1935
 
                    tt.create_file(contents, trans_id)
1936
 
                finally:
1937
 
                    contents.close()
1938
 
            count += 1
1939
 
        offset += count
1940
 
    for count, (trans_id, contents) in enumerate(tree.iter_files_bytes(
1941
 
                                                 new_desired_files)):
1942
 
        tt.create_file(contents, trans_id)
1943
 
        pb.update('Adding file contents', count + offset, total)
1944
1029
 
1945
1030
 
1946
1031
def _reparent_children(tt, old_parent, new_parent):
1947
1032
    for child in tt.iter_tree_children(old_parent):
1948
1033
        tt.adjust_path(tt.final_name(child), new_parent, child)
1949
1034
 
1950
 
def _reparent_transform_children(tt, old_parent, new_parent):
1951
 
    by_parent = tt.by_parent()
1952
 
    for child in by_parent[old_parent]:
1953
 
        tt.adjust_path(tt.final_name(child), new_parent, child)
1954
 
    return by_parent[old_parent]
1955
1035
 
1956
1036
def _content_match(tree, entry, file_id, kind, target_path):
1957
1037
    if entry.kind != kind:
1971
1051
    new_conflicts = set()
1972
1052
    for c_type, conflict in ((c[0], c) for c in conflicts):
1973
1053
        # Anything but a 'duplicate' would indicate programmer error
1974
 
        if c_type != 'duplicate':
1975
 
            raise AssertionError(c_type)
 
1054
        assert c_type == 'duplicate', c_type
1976
1055
        # Now figure out which is new and which is old
1977
1056
        if tt.new_contents(conflict[1]):
1978
1057
            new_file = conflict[1]
2006
1085
        executable = tree.is_executable(entry.file_id)
2007
1086
        return tt.new_file(name, parent_id, contents, entry.file_id, 
2008
1087
                           executable)
2009
 
    elif kind in ('directory', 'tree-reference'):
2010
 
        trans_id = tt.new_directory(name, parent_id, entry.file_id)
2011
 
        if kind == 'tree-reference':
2012
 
            tt.set_tree_reference(entry.reference_revision, trans_id)
2013
 
        return trans_id 
 
1088
    elif kind == 'directory':
 
1089
        return tt.new_directory(name, parent_id, entry.file_id)
2014
1090
    elif kind == 'symlink':
2015
1091
        target = tree.get_symlink_target(entry.file_id)
2016
1092
        return tt.new_symlink(name, parent_id, target, entry.file_id)
2017
 
    else:
2018
 
        raise errors.BadFileKindError(name, kind)
2019
 
 
2020
1093
 
2021
1094
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
2022
1095
    """Create new file contents according to an inventory entry."""
2029
1102
    elif entry.kind == "directory":
2030
1103
        tt.create_directory(trans_id)
2031
1104
 
2032
 
 
2033
1105
def create_entry_executability(tt, entry, trans_id):
2034
1106
    """Set the executability of a trans_id according to an inventory entry"""
2035
1107
    if entry.kind == "file":
2036
1108
        tt.set_executability(entry.executable, trans_id)
2037
1109
 
2038
1110
 
 
1111
def find_interesting(working_tree, target_tree, filenames):
 
1112
    """Find the ids corresponding to specified filenames."""
 
1113
    trees = (working_tree, target_tree)
 
1114
    return tree.find_ids_across_trees(filenames, trees)
 
1115
 
 
1116
 
 
1117
def change_entry(tt, file_id, working_tree, target_tree, 
 
1118
                 trans_id_file_id, backups, trans_id, by_parent):
 
1119
    """Replace a file_id's contents with those from a target tree."""
 
1120
    e_trans_id = trans_id_file_id(file_id)
 
1121
    entry = target_tree.inventory[file_id]
 
1122
    has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry, 
 
1123
                                                           working_tree)
 
1124
    if contents_mod:
 
1125
        mode_id = e_trans_id
 
1126
        if has_contents:
 
1127
            if not backups:
 
1128
                tt.delete_contents(e_trans_id)
 
1129
            else:
 
1130
                parent_trans_id = trans_id_file_id(entry.parent_id)
 
1131
                backup_name = get_backup_name(entry, by_parent,
 
1132
                                              parent_trans_id, tt)
 
1133
                tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
 
1134
                tt.unversion_file(e_trans_id)
 
1135
                e_trans_id = tt.create_path(entry.name, parent_trans_id)
 
1136
                tt.version_file(file_id, e_trans_id)
 
1137
                trans_id[file_id] = e_trans_id
 
1138
        create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
 
1139
        create_entry_executability(tt, entry, e_trans_id)
 
1140
 
 
1141
    elif meta_mod:
 
1142
        tt.set_executability(entry.executable, e_trans_id)
 
1143
    if tt.final_name(e_trans_id) != entry.name:
 
1144
        adjust_path  = True
 
1145
    else:
 
1146
        parent_id = tt.final_parent(e_trans_id)
 
1147
        parent_file_id = tt.final_file_id(parent_id)
 
1148
        if parent_file_id != entry.parent_id:
 
1149
            adjust_path = True
 
1150
        else:
 
1151
            adjust_path = False
 
1152
    if adjust_path:
 
1153
        parent_trans_id = trans_id_file_id(entry.parent_id)
 
1154
        tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
 
1155
 
 
1156
 
2039
1157
def get_backup_name(entry, by_parent, parent_trans_id, tt):
2040
1158
    return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
2041
1159
 
2079
1197
    return has_contents, contents_mod, meta_mod
2080
1198
 
2081
1199
 
2082
 
def revert(working_tree, target_tree, filenames, backups=False,
 
1200
def revert(working_tree, target_tree, filenames, backups=False, 
2083
1201
           pb=DummyProgress(), change_reporter=None):
2084
1202
    """Revert a working tree's contents to those of a target tree."""
2085
 
    target_tree.lock_read()
 
1203
    interesting_ids = find_interesting(working_tree, target_tree, filenames)
2086
1204
    tt = TreeTransform(working_tree, pb)
2087
1205
    try:
2088
1206
        pp = ProgressPhase("Revert phase", 3, pb)
2089
1207
        pp.next_phase()
2090
1208
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2091
1209
        try:
2092
 
            merge_modified = _alter_files(working_tree, target_tree, tt,
2093
 
                                          child_pb, filenames, backups)
 
1210
            _alter_files(working_tree, target_tree, tt, child_pb, 
 
1211
                         interesting_ids, backups, change_reporter)
2094
1212
        finally:
2095
1213
            child_pb.finished()
2096
1214
        pp.next_phase()
2097
1215
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2098
1216
        try:
2099
 
            raw_conflicts = resolve_conflicts(tt, child_pb,
2100
 
                lambda t, c: conflict_pass(t, c, target_tree))
 
1217
            raw_conflicts = resolve_conflicts(tt, child_pb)
2101
1218
        finally:
2102
1219
            child_pb.finished()
2103
1220
        conflicts = cook_conflicts(raw_conflicts, tt)
2104
 
        if change_reporter:
2105
 
            change_reporter = delta._ChangeReporter(
2106
 
                unversioned_filter=working_tree.is_ignored)
2107
 
            delta.report_changes(tt.iter_changes(), change_reporter)
2108
1221
        for conflict in conflicts:
2109
1222
            warning(conflict)
2110
1223
        pp.next_phase()
2111
1224
        tt.apply()
2112
 
        working_tree.set_merge_modified(merge_modified)
 
1225
        working_tree.set_merge_modified({})
2113
1226
    finally:
2114
 
        target_tree.unlock()
2115
1227
        tt.finalize()
2116
1228
        pb.clear()
2117
1229
    return conflicts
2118
1230
 
2119
1231
 
2120
 
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2121
 
                 backups):
 
1232
def _alter_files(working_tree, target_tree, tt, pb, interesting_ids, backups,
 
1233
                 report_changes):
 
1234
    from bzrlib import delta
2122
1235
    merge_modified = working_tree.merge_modified()
2123
 
    change_list = target_tree.iter_changes(working_tree,
2124
 
        specific_files=specific_files, pb=pb)
 
1236
    change_list = list(target_tree._iter_changes(working_tree,
 
1237
        specific_file_ids=interesting_ids, pb=pb))
2125
1238
    if target_tree.inventory.root is None:
2126
1239
        skip_root = True
2127
1240
    else:
2128
1241
        skip_root = False
2129
1242
    basis_tree = None
2130
 
    try:
2131
 
        deferred_files = []
2132
 
        for id_num, (file_id, path, changed_content, versioned, parent, name,
2133
 
                kind, executable) in enumerate(change_list):
2134
 
            if skip_root and file_id[0] is not None and parent[0] is None:
2135
 
                continue
2136
 
            trans_id = tt.trans_id_file_id(file_id)
2137
 
            mode_id = None
2138
 
            if changed_content:
2139
 
                keep_content = False
2140
 
                if kind[0] == 'file' and (backups or kind[1] is None):
2141
 
                    wt_sha1 = working_tree.get_file_sha1(file_id)
2142
 
                    if merge_modified.get(file_id) != wt_sha1:
2143
 
                        # acquire the basis tree lazily to prevent the
2144
 
                        # expense of accessing it when it's not needed ?
2145
 
                        # (Guessing, RBC, 200702)
2146
 
                        if basis_tree is None:
2147
 
                            basis_tree = working_tree.basis_tree()
2148
 
                            basis_tree.lock_read()
2149
 
                        if file_id in basis_tree:
2150
 
                            if wt_sha1 != basis_tree.get_file_sha1(file_id):
2151
 
                                keep_content = True
2152
 
                        elif kind[1] is None and not versioned[1]:
2153
 
                            keep_content = True
2154
 
                if kind[0] is not None:
2155
 
                    if not keep_content:
2156
 
                        tt.delete_contents(trans_id)
2157
 
                    elif kind[1] is not None:
2158
 
                        parent_trans_id = tt.trans_id_file_id(parent[0])
2159
 
                        by_parent = tt.by_parent()
2160
 
                        backup_name = _get_backup_name(name[0], by_parent,
2161
 
                                                       parent_trans_id, tt)
2162
 
                        tt.adjust_path(backup_name, parent_trans_id, trans_id)
2163
 
                        new_trans_id = tt.create_path(name[0], parent_trans_id)
2164
 
                        if versioned == (True, True):
2165
 
                            tt.unversion_file(trans_id)
2166
 
                            tt.version_file(file_id, new_trans_id)
2167
 
                        # New contents should have the same unix perms as old
2168
 
                        # contents
2169
 
                        mode_id = trans_id
2170
 
                        trans_id = new_trans_id
2171
 
                if kind[1] == 'directory':
2172
 
                    tt.create_directory(trans_id)
2173
 
                elif kind[1] == 'symlink':
2174
 
                    tt.create_symlink(target_tree.get_symlink_target(file_id),
2175
 
                                      trans_id)
2176
 
                elif kind[1] == 'file':
2177
 
                    deferred_files.append((file_id, (trans_id, mode_id)))
 
1243
    if report_changes:
 
1244
        change_reporter = delta.ChangeReporter(working_tree.inventory)
 
1245
        delta.report_changes(change_list, change_reporter)
 
1246
    for id_num, (file_id, path, changed_content, versioned, parent, name, kind,
 
1247
                 executable) in enumerate(change_list):
 
1248
        if skip_root and file_id[0] is not None and parent[0] is None:
 
1249
            continue
 
1250
        trans_id = tt.trans_id_file_id(file_id)
 
1251
        mode_id = None
 
1252
        if changed_content:
 
1253
            keep_content = False
 
1254
            if kind[0] == 'file' and (backups or kind[1] is None):
 
1255
                wt_sha1 = working_tree.get_file_sha1(file_id)
 
1256
                if merge_modified.get(file_id) != wt_sha1:
2178
1257
                    if basis_tree is None:
2179
1258
                        basis_tree = working_tree.basis_tree()
2180
 
                        basis_tree.lock_read()
2181
 
                    new_sha1 = target_tree.get_file_sha1(file_id)
2182
 
                    if (file_id in basis_tree and new_sha1 ==
2183
 
                        basis_tree.get_file_sha1(file_id)):
2184
 
                        if file_id in merge_modified:
2185
 
                            del merge_modified[file_id]
2186
 
                    else:
2187
 
                        merge_modified[file_id] = new_sha1
2188
 
 
2189
 
                    # preserve the execute bit when backing up
2190
 
                    if keep_content and executable[0] == executable[1]:
2191
 
                        tt.set_executability(executable[1], trans_id)
 
1259
                    if file_id in basis_tree:
 
1260
                        if wt_sha1 != basis_tree.get_file_sha1(file_id):
 
1261
                            keep_content = True
 
1262
                    elif kind[1] is None and not versioned[1]:
 
1263
                        keep_content = True
 
1264
            if kind[0] is not None:
 
1265
                if not keep_content:
 
1266
                    tt.delete_contents(trans_id)
2192
1267
                elif kind[1] is not None:
2193
 
                    raise AssertionError(kind[1])
2194
 
            if versioned == (False, True):
2195
 
                tt.version_file(file_id, trans_id)
2196
 
            if versioned == (True, False):
2197
 
                tt.unversion_file(trans_id)
2198
 
            if (name[1] is not None and 
2199
 
                (name[0] != name[1] or parent[0] != parent[1])):
2200
 
                tt.adjust_path(
2201
 
                    name[1], tt.trans_id_file_id(parent[1]), trans_id)
2202
 
            if executable[0] != executable[1] and kind[1] == "file":
2203
 
                tt.set_executability(executable[1], trans_id)
2204
 
        for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
2205
 
            deferred_files):
2206
 
            tt.create_file(bytes, trans_id, mode_id)
2207
 
    finally:
2208
 
        if basis_tree is not None:
2209
 
            basis_tree.unlock()
2210
 
    return merge_modified
 
1268
                    parent_trans_id = tt.trans_id_file_id(parent[0])
 
1269
                    by_parent = tt.by_parent()
 
1270
                    backup_name = _get_backup_name(name[0], by_parent,
 
1271
                                                   parent_trans_id, tt)
 
1272
                    tt.adjust_path(backup_name, parent_trans_id, trans_id)
 
1273
                    new_trans_id = tt.create_path(name[0], parent_trans_id)
 
1274
                    if versioned == (True, True):
 
1275
                        tt.unversion_file(trans_id)
 
1276
                        tt.version_file(file_id, new_trans_id)
 
1277
                    # New contents should have the same unix perms as old
 
1278
                    # contents
 
1279
                    mode_id = trans_id
 
1280
                    trans_id = new_trans_id
 
1281
            if kind[1] == 'directory':
 
1282
                tt.create_directory(trans_id)
 
1283
            elif kind[1] == 'symlink':
 
1284
                tt.create_symlink(target_tree.get_symlink_target(file_id),
 
1285
                                  trans_id)
 
1286
            elif kind[1] == 'file':
 
1287
                tt.create_file(target_tree.get_file_lines(file_id),
 
1288
                               trans_id, mode_id)
 
1289
                # preserve the execute bit when backing up
 
1290
                if keep_content and executable[0] == executable[1]:
 
1291
                    tt.set_executability(executable[1], trans_id)
 
1292
            else:
 
1293
                assert kind[1] is None
 
1294
        if versioned == (False, True):
 
1295
            tt.version_file(file_id, trans_id)
 
1296
        if versioned == (True, False):
 
1297
            tt.unversion_file(trans_id)
 
1298
        if (name[1] is not None and 
 
1299
            (name[0] != name[1] or parent[0] != parent[1])):
 
1300
            tt.adjust_path(name[1], tt.trans_id_file_id(parent[1]), trans_id)
 
1301
        if executable[0] != executable[1] and kind[1] == "file":
 
1302
            tt.set_executability(executable[1], trans_id)
2211
1303
 
2212
1304
 
2213
1305
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
2227
1319
        pb.clear()
2228
1320
 
2229
1321
 
2230
 
def conflict_pass(tt, conflicts, path_tree=None):
2231
 
    """Resolve some classes of conflicts.
2232
 
 
2233
 
    :param tt: The transform to resolve conflicts in
2234
 
    :param conflicts: The conflicts to resolve
2235
 
    :param path_tree: A Tree to get supplemental paths from
2236
 
    """
 
1322
def conflict_pass(tt, conflicts):
 
1323
    """Resolve some classes of conflicts."""
2237
1324
    new_conflicts = set()
2238
1325
    for c_type, conflict in ((c[0], c) for c in conflicts):
2239
1326
        if c_type == 'duplicate id':
2242
1329
                               conflict[1], conflict[2], ))
2243
1330
        elif c_type == 'duplicate':
2244
1331
            # files that were renamed take precedence
 
1332
            new_name = tt.final_name(conflict[1])+'.moved'
2245
1333
            final_parent = tt.final_parent(conflict[1])
2246
1334
            if tt.path_changed(conflict[1]):
2247
 
                existing_file, new_file = conflict[2], conflict[1]
 
1335
                tt.adjust_path(new_name, final_parent, conflict[2])
 
1336
                new_conflicts.add((c_type, 'Moved existing file to', 
 
1337
                                   conflict[2], conflict[1]))
2248
1338
            else:
2249
 
                existing_file, new_file = conflict[1], conflict[2]
2250
 
            new_name = tt.final_name(existing_file)+'.moved'
2251
 
            tt.adjust_path(new_name, final_parent, existing_file)
2252
 
            new_conflicts.add((c_type, 'Moved existing file to', 
2253
 
                               existing_file, new_file))
 
1339
                tt.adjust_path(new_name, final_parent, conflict[1])
 
1340
                new_conflicts.add((c_type, 'Moved existing file to', 
 
1341
                                  conflict[1], conflict[2]))
2254
1342
        elif c_type == 'parent loop':
2255
1343
            # break the loop by undoing one of the ops that caused the loop
2256
1344
            cur = conflict[1]
2267
1355
                new_conflicts.add(('deleting parent', 'Not deleting', 
2268
1356
                                   trans_id))
2269
1357
            except KeyError:
2270
 
                create = True
2271
 
                try:
2272
 
                    tt.final_name(trans_id)
2273
 
                except NoFinalPath:
2274
 
                    if path_tree is not None:
2275
 
                        file_id = tt.final_file_id(trans_id)
2276
 
                        if file_id is None:
2277
 
                            file_id = tt.inactive_file_id(trans_id)
2278
 
                        entry = path_tree.inventory[file_id]
2279
 
                        # special-case the other tree root (move its
2280
 
                        # children to current root)
2281
 
                        if entry.parent_id is None:
2282
 
                            create=False
2283
 
                            moved = _reparent_transform_children(
2284
 
                                tt, trans_id, tt.root)
2285
 
                            for child in moved:
2286
 
                                new_conflicts.add((c_type, 'Moved to root',
2287
 
                                                   child))
2288
 
                        else:
2289
 
                            parent_trans_id = tt.trans_id_file_id(
2290
 
                                entry.parent_id)
2291
 
                            tt.adjust_path(entry.name, parent_trans_id,
2292
 
                                           trans_id)
2293
 
                if create:
2294
 
                    tt.create_directory(trans_id)
2295
 
                    new_conflicts.add((c_type, 'Created directory', trans_id))
 
1358
                tt.create_directory(trans_id)
 
1359
                new_conflicts.add((c_type, 'Created directory', trans_id))
2296
1360
        elif c_type == 'unversioned parent':
2297
 
            file_id = tt.inactive_file_id(conflict[1])
2298
 
            # special-case the other tree root (move its children instead)
2299
 
            if path_tree and file_id in path_tree:
2300
 
                if path_tree.inventory[file_id].parent_id is None:
2301
 
                    continue
2302
 
            tt.version_file(file_id, conflict[1])
 
1361
            tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
2303
1362
            new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
2304
 
        elif c_type == 'non-directory parent':
2305
 
            parent_id = conflict[1]
2306
 
            parent_parent = tt.final_parent(parent_id)
2307
 
            parent_name = tt.final_name(parent_id)
2308
 
            parent_file_id = tt.final_file_id(parent_id)
2309
 
            new_parent_id = tt.new_directory(parent_name + '.new',
2310
 
                parent_parent, parent_file_id)
2311
 
            _reparent_transform_children(tt, parent_id, new_parent_id)
2312
 
            if parent_file_id is not None:
2313
 
                tt.unversion_file(parent_id)
2314
 
            new_conflicts.add((c_type, 'Created directory', new_parent_id))
2315
1363
    return new_conflicts
2316
1364
 
2317
1365
 
2341
1389
                                   file_id=modified_id, 
2342
1390
                                   conflict_path=conflicting_path,
2343
1391
                                   conflict_file_id=conflicting_id)
2344
 
 
2345
 
 
2346
 
class _FileMover(object):
2347
 
    """Moves and deletes files for TreeTransform, tracking operations"""
2348
 
 
2349
 
    def __init__(self):
2350
 
        self.past_renames = []
2351
 
        self.pending_deletions = []
2352
 
 
2353
 
    def rename(self, from_, to):
2354
 
        """Rename a file from one path to another.  Functions like os.rename"""
2355
 
        try:
2356
 
            os.rename(from_, to)
2357
 
        except OSError, e:
2358
 
            if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
2359
 
                raise errors.FileExists(to, str(e))
2360
 
            raise
2361
 
        self.past_renames.append((from_, to))
2362
 
 
2363
 
    def pre_delete(self, from_, to):
2364
 
        """Rename a file out of the way and mark it for deletion.
2365
 
 
2366
 
        Unlike os.unlink, this works equally well for files and directories.
2367
 
        :param from_: The current file path
2368
 
        :param to: A temporary path for the file
2369
 
        """
2370
 
        self.rename(from_, to)
2371
 
        self.pending_deletions.append(to)
2372
 
 
2373
 
    def rollback(self):
2374
 
        """Reverse all renames that have been performed"""
2375
 
        for from_, to in reversed(self.past_renames):
2376
 
            os.rename(to, from_)
2377
 
        # after rollback, don't reuse _FileMover
2378
 
        past_renames = None
2379
 
        pending_deletions = None
2380
 
 
2381
 
    def apply_deletions(self):
2382
 
        """Apply all marked deletions"""
2383
 
        for path in self.pending_deletions:
2384
 
            delete_any(path)
2385
 
        # after apply_deletions, don't reuse _FileMover
2386
 
        past_renames = None
2387
 
        pending_deletions = None