/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/transform.py

  • Committer: Andrew Bennetts
  • Date: 2008-03-27 06:10:18 UTC
  • mfrom: (3309 +trunk)
  • mto: This revision was merged to the branch mainline in revision 3320.
  • Revision ID: andrew.bennetts@canonical.com-20080327061018-dxztpxyv6yoeg3am
Merge from bzr.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2006, 2007, 2008 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
17
17
import os
18
18
import errno
19
19
from stat import S_ISREG
 
20
import tempfile
20
21
 
21
22
from bzrlib.lazy_import import lazy_import
22
23
lazy_import(globals(), """
62
63
        self.rename_count = rename_count
63
64
 
64
65
 
65
 
class TreeTransform(object):
66
 
    """Represent a tree transformation.
67
 
    
68
 
    This object is designed to support incremental generation of the transform,
69
 
    in any order.
70
 
 
71
 
    However, it gives optimum performance when parent directories are created
72
 
    before their contents.  The transform is then able to put child files
73
 
    directly in their parent directory, avoiding later renames.
74
 
    
75
 
    It is easy to produce malformed transforms, but they are generally
76
 
    harmless.  Attempting to apply a malformed transform will cause an
77
 
    exception to be raised before any modifications are made to the tree.  
78
 
 
79
 
    Many kinds of malformed transforms can be corrected with the 
80
 
    resolve_conflicts function.  The remaining ones indicate programming error,
81
 
    such as trying to create a file with no path.
82
 
 
83
 
    Two sets of file creation methods are supplied.  Convenience methods are:
84
 
     * new_file
85
 
     * new_directory
86
 
     * new_symlink
87
 
 
88
 
    These are composed of the low-level methods:
89
 
     * create_path
90
 
     * create_file or create_directory or create_symlink
91
 
     * version_file
92
 
     * set_executability
93
 
 
94
 
    Transform/Transaction ids
95
 
    -------------------------
96
 
    trans_ids are temporary ids assigned to all files involved in a transform.
97
 
    It's possible, even common, that not all files in the Tree have trans_ids.
98
 
 
99
 
    trans_ids are used because filenames and file_ids are not good enough
100
 
    identifiers; filenames change, and not all files have file_ids.  File-ids
101
 
    are also associated with trans-ids, so that moving a file moves its
102
 
    file-id.
103
 
 
104
 
    trans_ids are only valid for the TreeTransform that generated them.
105
 
 
106
 
    Limbo
107
 
    -----
108
 
    Limbo is a temporary directory use to hold new versions of files.
109
 
    Files are added to limbo by create_file, create_directory, create_symlink,
110
 
    and their convenience variants (new_*).  Files may be removed from limbo
111
 
    using cancel_creation.  Files are renamed from limbo into their final
112
 
    location as part of TreeTransform.apply
113
 
 
114
 
    Limbo must be cleaned up, by either calling TreeTransform.apply or
115
 
    calling TreeTransform.finalize.
116
 
 
117
 
    Files are placed into limbo inside their parent directories, where
118
 
    possible.  This reduces subsequent renames, and makes operations involving
119
 
    lots of files faster.  This optimization is only possible if the parent
120
 
    directory is created *before* creating any of its children, so avoid
121
 
    creating children before parents, where possible.
122
 
 
123
 
    Pending-deletion
124
 
    ----------------
125
 
    This temporary directory is used by _FileMover for storing files that are
126
 
    about to be deleted.  In case of rollback, the files will be restored.
127
 
    FileMover does not delete files until it is sure that a rollback will not
128
 
    happen.  
129
 
    """
130
 
    def __init__(self, tree, pb=DummyProgress()):
131
 
        """Note: a tree_write lock is taken on the tree.
132
 
        
133
 
        Use TreeTransform.finalize() to release the lock (can be omitted if
134
 
        TreeTransform.apply() called).
 
66
class TreeTransformBase(object):
 
67
    """The base class for TreeTransform and TreeTransformBase"""
 
68
 
 
69
    def __init__(self, tree, limbodir, pb=DummyProgress(),
 
70
                 case_sensitive=True):
 
71
        """Constructor.
 
72
 
 
73
        :param tree: The tree that will be transformed, but not necessarily
 
74
            the output tree.
 
75
        :param limbodir: A directory where new files can be stored until
 
76
            they are installed in their proper places
 
77
        :param pb: A ProgressBar indicating how much progress is being made
 
78
        :param case_sensitive: If True, the target of the transform is
 
79
            case sensitive, not just case preserving.
135
80
        """
136
81
        object.__init__(self)
137
82
        self._tree = tree
138
 
        self._tree.lock_tree_write()
139
 
        try:
140
 
            control_files = self._tree._control_files
141
 
            self._limbodir = urlutils.local_path_from_url(
142
 
                control_files.controlfilename('limbo'))
143
 
            try:
144
 
                os.mkdir(self._limbodir)
145
 
            except OSError, e:
146
 
                if e.errno == errno.EEXIST:
147
 
                    raise ExistingLimbo(self._limbodir)
148
 
            self._deletiondir = urlutils.local_path_from_url(
149
 
                control_files.controlfilename('pending-deletion'))
150
 
            try:
151
 
                os.mkdir(self._deletiondir)
152
 
            except OSError, e:
153
 
                if e.errno == errno.EEXIST:
154
 
                    raise errors.ExistingPendingDeletion(self._deletiondir)
155
 
 
156
 
        except: 
157
 
            self._tree.unlock()
158
 
            raise
159
 
 
160
 
        # counter used to generate trans-ids (which are locally unique)
 
83
        self._limbodir = limbodir
 
84
        self._deletiondir = None
161
85
        self._id_number = 0
162
86
        # mapping of trans_id -> new basename
163
87
        self._new_name = {}
199
123
        # The trans_id that will be used as the tree root
200
124
        self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
201
125
        # Indictor of whether the transform has been applied
202
 
        self.__done = False
 
126
        self._done = False
203
127
        # A progress bar
204
128
        self._pb = pb
 
129
        # Whether the target is case sensitive
 
130
        self._case_sensitive_target = case_sensitive
205
131
        # A counter of how many files have been renamed
206
132
        self.rename_count = 0
207
133
 
233
159
                # We don't especially care *why* the dir is immortal.
234
160
                raise ImmortalLimbo(self._limbodir)
235
161
            try:
236
 
                os.rmdir(self._deletiondir)
 
162
                if self._deletiondir is not None:
 
163
                    os.rmdir(self._deletiondir)
237
164
            except OSError:
238
165
                raise errors.ImmortalPendingDeletion(self._deletiondir)
239
166
        finally:
323
250
        This reflects only files that already exist, not ones that will be
324
251
        added by transactions.
325
252
        """
326
 
        path = self._tree.inventory.id2path(inventory_id)
 
253
        path = self._tree.id2path(inventory_id)
327
254
        return self.trans_id_tree_path(path)
328
255
 
329
256
    def trans_id_file_id(self, file_id):
427
354
        if typefunc(mode):
428
355
            os.chmod(self._limbo_name(trans_id), mode)
429
356
 
 
357
    def create_hardlink(self, path, trans_id):
 
358
        """Schedule creation of a hard link"""
 
359
        name = self._limbo_name(trans_id)
 
360
        try:
 
361
            os.link(path, name)
 
362
        except OSError, e:
 
363
            if e.errno != errno.EPERM:
 
364
                raise
 
365
            raise errors.HardLinkNotSupported(path)
 
366
        try:
 
367
            unique_add(self._new_contents, trans_id, 'file')
 
368
        except:
 
369
            # Clean up the file, it never got registered so
 
370
            # TreeTransform.finalize() won't clean it up.
 
371
            os.unlink(name)
 
372
            raise
 
373
 
430
374
    def create_directory(self, trans_id):
431
375
        """Schedule creation of a new directory.
432
376
        
630
574
 
631
575
    def find_conflicts(self):
632
576
        """Find any violations of inventory or filesystem invariants"""
633
 
        if self.__done is True:
 
577
        if self._done is True:
634
578
            raise ReusingTransform()
635
579
        conflicts = []
636
580
        # ensure all children of all existent parents are known
801
745
            return conflicts
802
746
        for children in by_parent.itervalues():
803
747
            name_ids = [(self.final_name(t), t) for t in children]
804
 
            if not self._tree.case_sensitive:
 
748
            if not self._case_sensitive_target:
805
749
                name_ids = [(n.lower(), t) for n, t in name_ids]
806
750
            name_ids.sort()
807
751
            last_name = None
826
770
        conflicts = []
827
771
        removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
828
772
                                self._removed_id))
829
 
        active_tree_ids = set((f for f in self._tree.inventory if
830
 
                               f not in removed_tree_ids))
 
773
        all_ids = self._tree.all_file_ids()
 
774
        active_tree_ids = all_ids.difference(removed_tree_ids)
831
775
        for trans_id, file_id in self._new_id.iteritems():
832
776
            if file_id in active_tree_ids:
833
777
                old_trans_id = self.trans_id_tree_file_id(file_id)
866
810
                continue
867
811
            return True
868
812
        return False
869
 
            
870
 
    def apply(self, no_conflicts=False, _mover=None):
871
 
        """Apply all changes to the inventory and filesystem.
872
 
        
873
 
        If filesystem or inventory conflicts are present, MalformedTransform
874
 
        will be thrown.
875
 
 
876
 
        If apply succeeds, finalize is not necessary.
877
 
 
878
 
        :param no_conflicts: if True, the caller guarantees there are no
879
 
            conflicts, so no check is made.
880
 
        :param _mover: Supply an alternate FileMover, for testing
881
 
        """
882
 
        if not no_conflicts:
883
 
            conflicts = self.find_conflicts()
884
 
            if len(conflicts) != 0:
885
 
                raise MalformedTransform(conflicts=conflicts)
886
 
        inv = self._tree.inventory
887
 
        inventory_delta = []
888
 
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
889
 
        try:
890
 
            if _mover is None:
891
 
                mover = _FileMover()
892
 
            else:
893
 
                mover = _mover
894
 
            try:
895
 
                child_pb.update('Apply phase', 0, 2)
896
 
                self._apply_removals(inv, inventory_delta, mover)
897
 
                child_pb.update('Apply phase', 1, 2)
898
 
                modified_paths = self._apply_insertions(inv, inventory_delta,
899
 
                                                        mover)
900
 
            except:
901
 
                mover.rollback()
902
 
                raise
903
 
            else:
904
 
                mover.apply_deletions()
905
 
        finally:
906
 
            child_pb.finished()
907
 
        self._tree.apply_inventory_delta(inventory_delta)
908
 
        self.__done = True
909
 
        self.finalize()
910
 
        return _TransformResults(modified_paths, self.rename_count)
911
813
 
912
814
    def _limbo_name(self, trans_id):
913
815
        """Generate the limbo name of a file"""
929
831
                # the direct path can only be used if no other file has
930
832
                # already taken this pathname, i.e. if the name is unused, or
931
833
                # if it is already associated with this trans_id.
932
 
                elif self._tree.case_sensitive:
 
834
                elif self._case_sensitive_target:
933
835
                    if (self._limbo_children_names[parent].get(filename)
934
836
                        in (trans_id, None)):
935
837
                        use_direct_path = True
953
855
        self._limbo_files[trans_id] = limbo_name
954
856
        return limbo_name
955
857
 
956
 
    def _apply_removals(self, inv, inventory_delta, mover):
957
 
        """Perform tree operations that remove directory/inventory names.
958
 
        
959
 
        That is, delete files that are to be deleted, and put any files that
960
 
        need renaming into limbo.  This must be done in strict child-to-parent
961
 
        order.
962
 
        """
963
 
        tree_paths = list(self._tree_path_ids.iteritems())
964
 
        tree_paths.sort(reverse=True)
965
 
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
966
 
        try:
967
 
            for num, data in enumerate(tree_paths):
968
 
                path, trans_id = data
969
 
                child_pb.update('removing file', num, len(tree_paths))
970
 
                full_path = self._tree.abspath(path)
971
 
                if trans_id in self._removed_contents:
972
 
                    mover.pre_delete(full_path, os.path.join(self._deletiondir,
973
 
                                     trans_id))
974
 
                elif trans_id in self._new_name or trans_id in \
975
 
                    self._new_parent:
976
 
                    try:
977
 
                        mover.rename(full_path, self._limbo_name(trans_id))
978
 
                    except OSError, e:
979
 
                        if e.errno != errno.ENOENT:
980
 
                            raise
981
 
                    else:
982
 
                        self.rename_count += 1
983
 
                if trans_id in self._removed_id:
984
 
                    if trans_id == self._new_root:
985
 
                        file_id = self._tree.get_root_id()
986
 
                    else:
987
 
                        file_id = self.tree_file_id(trans_id)
988
 
                    if file_id is not None:
989
 
                        inventory_delta.append((path, None, file_id, None))
990
 
        finally:
991
 
            child_pb.finished()
992
 
 
993
 
    def _apply_insertions(self, inv, inventory_delta, mover):
994
 
        """Perform tree operations that insert directory/inventory names.
995
 
        
996
 
        That is, create any files that need to be created, and restore from
997
 
        limbo any files that needed renaming.  This must be done in strict
998
 
        parent-to-child order.
999
 
        """
1000
 
        new_paths = self.new_paths()
1001
 
        modified_paths = []
1002
 
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1003
 
        completed_new = []
1004
 
        try:
1005
 
            for num, (path, trans_id) in enumerate(new_paths):
1006
 
                new_entry = None
1007
 
                child_pb.update('adding file', num, len(new_paths))
1008
 
                try:
1009
 
                    kind = self._new_contents[trans_id]
1010
 
                except KeyError:
1011
 
                    kind = contents = None
1012
 
                if trans_id in self._new_contents or \
1013
 
                    self.path_changed(trans_id):
1014
 
                    full_path = self._tree.abspath(path)
1015
 
                    if trans_id in self._needs_rename:
1016
 
                        try:
1017
 
                            mover.rename(self._limbo_name(trans_id), full_path)
1018
 
                        except OSError, e:
1019
 
                            # We may be renaming a dangling inventory id
1020
 
                            if e.errno != errno.ENOENT:
1021
 
                                raise
1022
 
                        else:
1023
 
                            self.rename_count += 1
1024
 
                    if trans_id in self._new_contents:
1025
 
                        modified_paths.append(full_path)
1026
 
                        completed_new.append(trans_id)
1027
 
 
1028
 
                if trans_id in self._new_id:
1029
 
                    if kind is None:
1030
 
                        kind = file_kind(self._tree.abspath(path))
1031
 
                    if trans_id in self._new_reference_revision:
1032
 
                        new_entry = inventory.TreeReference(
1033
 
                            self._new_id[trans_id],
1034
 
                            self._new_name[trans_id], 
1035
 
                            self.final_file_id(self._new_parent[trans_id]),
1036
 
                            None, self._new_reference_revision[trans_id])
1037
 
                    else:
1038
 
                        new_entry = inventory.make_entry(kind,
1039
 
                            self.final_name(trans_id),
1040
 
                            self.final_file_id(self.final_parent(trans_id)),
1041
 
                            self._new_id[trans_id])
1042
 
                else:
1043
 
                    if trans_id in self._new_name or trans_id in\
1044
 
                        self._new_parent or\
1045
 
                        trans_id in self._new_executability:
1046
 
                        file_id = self.final_file_id(trans_id)
1047
 
                        if file_id is not None:
1048
 
                            entry = inv[file_id]
1049
 
                            new_entry = entry.copy()
1050
 
 
1051
 
                    if trans_id in self._new_name or trans_id in\
1052
 
                        self._new_parent:
1053
 
                            if new_entry is not None:
1054
 
                                new_entry.name = self.final_name(trans_id)
1055
 
                                parent = self.final_parent(trans_id)
1056
 
                                parent_id = self.final_file_id(parent)
1057
 
                                new_entry.parent_id = parent_id
1058
 
 
1059
 
                if trans_id in self._new_executability:
1060
 
                    self._set_executability(path, new_entry, trans_id)
1061
 
                if new_entry is not None:
1062
 
                    if new_entry.file_id in inv:
1063
 
                        old_path = inv.id2path(new_entry.file_id)
1064
 
                    else:
1065
 
                        old_path = None
1066
 
                    inventory_delta.append((old_path, path,
1067
 
                                            new_entry.file_id,
1068
 
                                            new_entry))
1069
 
        finally:
1070
 
            child_pb.finished()
1071
 
        for trans_id in completed_new:
1072
 
            del self._new_contents[trans_id]
1073
 
        return modified_paths
1074
 
 
1075
858
    def _set_executability(self, path, entry, trans_id):
1076
859
        """Set the executability of versioned files """
1077
860
        new_executability = self._new_executability[trans_id]
1219
1002
            to_executable = False
1220
1003
        return to_name, to_parent, to_kind, to_executable
1221
1004
 
1222
 
    def _iter_changes(self):
1223
 
        """Produce output in the same format as Tree._iter_changes.
 
1005
    def iter_changes(self):
 
1006
        """Produce output in the same format as Tree.iter_changes.
1224
1007
 
1225
1008
        Will produce nonsensical results if invoked while inventory/filesystem
1226
1009
        conflicts (as reported by TreeTransform.find_conflicts()) are present.
1281
1064
                   (from_executable, to_executable)))
1282
1065
        return iter(sorted(results, key=lambda x:x[1]))
1283
1066
 
 
1067
    def get_preview_tree(self):
 
1068
        """Return a tree representing the result of the transform.
 
1069
 
 
1070
        This tree only supports the subset of Tree functionality required
 
1071
        by show_diff_trees.  It must only be compared to tt._tree.
 
1072
        """
 
1073
        return _PreviewTree(self)
 
1074
 
 
1075
 
 
1076
class TreeTransform(TreeTransformBase):
 
1077
    """Represent a tree transformation.
 
1078
 
 
1079
    This object is designed to support incremental generation of the transform,
 
1080
    in any order.
 
1081
 
 
1082
    However, it gives optimum performance when parent directories are created
 
1083
    before their contents.  The transform is then able to put child files
 
1084
    directly in their parent directory, avoiding later renames.
 
1085
 
 
1086
    It is easy to produce malformed transforms, but they are generally
 
1087
    harmless.  Attempting to apply a malformed transform will cause an
 
1088
    exception to be raised before any modifications are made to the tree.
 
1089
 
 
1090
    Many kinds of malformed transforms can be corrected with the
 
1091
    resolve_conflicts function.  The remaining ones indicate programming error,
 
1092
    such as trying to create a file with no path.
 
1093
 
 
1094
    Two sets of file creation methods are supplied.  Convenience methods are:
 
1095
     * new_file
 
1096
     * new_directory
 
1097
     * new_symlink
 
1098
 
 
1099
    These are composed of the low-level methods:
 
1100
     * create_path
 
1101
     * create_file or create_directory or create_symlink
 
1102
     * version_file
 
1103
     * set_executability
 
1104
 
 
1105
    Transform/Transaction ids
 
1106
    -------------------------
 
1107
    trans_ids are temporary ids assigned to all files involved in a transform.
 
1108
    It's possible, even common, that not all files in the Tree have trans_ids.
 
1109
 
 
1110
    trans_ids are used because filenames and file_ids are not good enough
 
1111
    identifiers; filenames change, and not all files have file_ids.  File-ids
 
1112
    are also associated with trans-ids, so that moving a file moves its
 
1113
    file-id.
 
1114
 
 
1115
    trans_ids are only valid for the TreeTransform that generated them.
 
1116
 
 
1117
    Limbo
 
1118
    -----
 
1119
    Limbo is a temporary directory use to hold new versions of files.
 
1120
    Files are added to limbo by create_file, create_directory, create_symlink,
 
1121
    and their convenience variants (new_*).  Files may be removed from limbo
 
1122
    using cancel_creation.  Files are renamed from limbo into their final
 
1123
    location as part of TreeTransform.apply
 
1124
 
 
1125
    Limbo must be cleaned up, by either calling TreeTransform.apply or
 
1126
    calling TreeTransform.finalize.
 
1127
 
 
1128
    Files are placed into limbo inside their parent directories, where
 
1129
    possible.  This reduces subsequent renames, and makes operations involving
 
1130
    lots of files faster.  This optimization is only possible if the parent
 
1131
    directory is created *before* creating any of its children, so avoid
 
1132
    creating children before parents, where possible.
 
1133
 
 
1134
    Pending-deletion
 
1135
    ----------------
 
1136
    This temporary directory is used by _FileMover for storing files that are
 
1137
    about to be deleted.  In case of rollback, the files will be restored.
 
1138
    FileMover does not delete files until it is sure that a rollback will not
 
1139
    happen.
 
1140
    """
 
1141
    def __init__(self, tree, pb=DummyProgress()):
 
1142
        """Note: a tree_write lock is taken on the tree.
 
1143
 
 
1144
        Use TreeTransform.finalize() to release the lock (can be omitted if
 
1145
        TreeTransform.apply() called).
 
1146
        """
 
1147
        tree.lock_tree_write()
 
1148
 
 
1149
        try:
 
1150
            control_files = tree._control_files
 
1151
            limbodir = urlutils.local_path_from_url(
 
1152
                control_files.controlfilename('limbo'))
 
1153
            try:
 
1154
                os.mkdir(limbodir)
 
1155
            except OSError, e:
 
1156
                if e.errno == errno.EEXIST:
 
1157
                    raise ExistingLimbo(limbodir)
 
1158
            deletiondir = urlutils.local_path_from_url(
 
1159
                control_files.controlfilename('pending-deletion'))
 
1160
            try:
 
1161
                os.mkdir(deletiondir)
 
1162
            except OSError, e:
 
1163
                if e.errno == errno.EEXIST:
 
1164
                    raise errors.ExistingPendingDeletion(deletiondir)
 
1165
        except:
 
1166
            tree.unlock()
 
1167
            raise
 
1168
 
 
1169
        TreeTransformBase.__init__(self, tree, limbodir, pb,
 
1170
                                   tree.case_sensitive)
 
1171
        self._deletiondir = deletiondir
 
1172
 
 
1173
    def apply(self, no_conflicts=False, _mover=None):
 
1174
        """Apply all changes to the inventory and filesystem.
 
1175
 
 
1176
        If filesystem or inventory conflicts are present, MalformedTransform
 
1177
        will be thrown.
 
1178
 
 
1179
        If apply succeeds, finalize is not necessary.
 
1180
 
 
1181
        :param no_conflicts: if True, the caller guarantees there are no
 
1182
            conflicts, so no check is made.
 
1183
        :param _mover: Supply an alternate FileMover, for testing
 
1184
        """
 
1185
        if not no_conflicts:
 
1186
            conflicts = self.find_conflicts()
 
1187
            if len(conflicts) != 0:
 
1188
                raise MalformedTransform(conflicts=conflicts)
 
1189
        inventory_delta = []
 
1190
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1191
        try:
 
1192
            if _mover is None:
 
1193
                mover = _FileMover()
 
1194
            else:
 
1195
                mover = _mover
 
1196
            try:
 
1197
                child_pb.update('Apply phase', 0, 2)
 
1198
                self._apply_removals(inventory_delta, mover)
 
1199
                child_pb.update('Apply phase', 1, 2)
 
1200
                modified_paths = self._apply_insertions(inventory_delta, mover)
 
1201
            except:
 
1202
                mover.rollback()
 
1203
                raise
 
1204
            else:
 
1205
                mover.apply_deletions()
 
1206
        finally:
 
1207
            child_pb.finished()
 
1208
        self._tree.apply_inventory_delta(inventory_delta)
 
1209
        self._done = True
 
1210
        self.finalize()
 
1211
        return _TransformResults(modified_paths, self.rename_count)
 
1212
 
 
1213
    def _apply_removals(self, inventory_delta, mover):
 
1214
        """Perform tree operations that remove directory/inventory names.
 
1215
 
 
1216
        That is, delete files that are to be deleted, and put any files that
 
1217
        need renaming into limbo.  This must be done in strict child-to-parent
 
1218
        order.
 
1219
        """
 
1220
        tree_paths = list(self._tree_path_ids.iteritems())
 
1221
        tree_paths.sort(reverse=True)
 
1222
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1223
        try:
 
1224
            for num, data in enumerate(tree_paths):
 
1225
                path, trans_id = data
 
1226
                child_pb.update('removing file', num, len(tree_paths))
 
1227
                full_path = self._tree.abspath(path)
 
1228
                if trans_id in self._removed_contents:
 
1229
                    mover.pre_delete(full_path, os.path.join(self._deletiondir,
 
1230
                                     trans_id))
 
1231
                elif trans_id in self._new_name or trans_id in \
 
1232
                    self._new_parent:
 
1233
                    try:
 
1234
                        mover.rename(full_path, self._limbo_name(trans_id))
 
1235
                    except OSError, e:
 
1236
                        if e.errno != errno.ENOENT:
 
1237
                            raise
 
1238
                    else:
 
1239
                        self.rename_count += 1
 
1240
                if trans_id in self._removed_id:
 
1241
                    if trans_id == self._new_root:
 
1242
                        file_id = self._tree.get_root_id()
 
1243
                    else:
 
1244
                        file_id = self.tree_file_id(trans_id)
 
1245
                    assert file_id is not None
 
1246
                    # File-id isn't really being deleted, just moved
 
1247
                    if file_id in self._r_new_id:
 
1248
                        continue
 
1249
                    inventory_delta.append((path, None, file_id, None))
 
1250
        finally:
 
1251
            child_pb.finished()
 
1252
 
 
1253
    def _apply_insertions(self, inventory_delta, mover):
 
1254
        """Perform tree operations that insert directory/inventory names.
 
1255
 
 
1256
        That is, create any files that need to be created, and restore from
 
1257
        limbo any files that needed renaming.  This must be done in strict
 
1258
        parent-to-child order.
 
1259
        """
 
1260
        new_paths = self.new_paths()
 
1261
        modified_paths = []
 
1262
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1263
        completed_new = []
 
1264
        try:
 
1265
            for num, (path, trans_id) in enumerate(new_paths):
 
1266
                new_entry = None
 
1267
                child_pb.update('adding file', num, len(new_paths))
 
1268
                if trans_id in self._new_contents or \
 
1269
                    self.path_changed(trans_id):
 
1270
                    full_path = self._tree.abspath(path)
 
1271
                    if trans_id in self._needs_rename:
 
1272
                        try:
 
1273
                            mover.rename(self._limbo_name(trans_id), full_path)
 
1274
                        except OSError, e:
 
1275
                            # We may be renaming a dangling inventory id
 
1276
                            if e.errno != errno.ENOENT:
 
1277
                                raise
 
1278
                        else:
 
1279
                            self.rename_count += 1
 
1280
                    if trans_id in self._new_contents:
 
1281
                        modified_paths.append(full_path)
 
1282
                        completed_new.append(trans_id)
 
1283
                file_id = self.final_file_id(trans_id)
 
1284
                if file_id is not None and (trans_id in self._new_id or
 
1285
                    trans_id in self._new_name or trans_id in self._new_parent
 
1286
                    or trans_id in self._new_executability):
 
1287
                    try:
 
1288
                        kind = self.final_kind(trans_id)
 
1289
                    except NoSuchFile:
 
1290
                        kind = self._tree.stored_kind(file_id)
 
1291
                    if trans_id in self._new_reference_revision:
 
1292
                        new_entry = inventory.TreeReference(
 
1293
                            self.final_file_id(trans_id),
 
1294
                            self._new_name[trans_id],
 
1295
                            self.final_file_id(self._new_parent[trans_id]),
 
1296
                            None, self._new_reference_revision[trans_id])
 
1297
                    else:
 
1298
                        new_entry = inventory.make_entry(kind,
 
1299
                            self.final_name(trans_id),
 
1300
                            self.final_file_id(self.final_parent(trans_id)),
 
1301
                            self.final_file_id(trans_id))
 
1302
                    try:
 
1303
                        old_path = self._tree.id2path(new_entry.file_id)
 
1304
                    except errors.NoSuchId:
 
1305
                        old_path = None
 
1306
                    inventory_delta.append((old_path, path, new_entry.file_id,
 
1307
                                            new_entry))
 
1308
 
 
1309
                if trans_id in self._new_executability:
 
1310
                    self._set_executability(path, new_entry, trans_id)
 
1311
        finally:
 
1312
            child_pb.finished()
 
1313
        for trans_id in completed_new:
 
1314
            del self._new_contents[trans_id]
 
1315
        return modified_paths
 
1316
 
 
1317
 
 
1318
class TransformPreview(TreeTransformBase):
 
1319
    """A TreeTransform for generating preview trees.
 
1320
 
 
1321
    Unlike TreeTransform, this version works when the input tree is a
 
1322
    RevisionTree, rather than a WorkingTree.  As a result, it tends to ignore
 
1323
    unversioned files in the input tree.
 
1324
    """
 
1325
 
 
1326
    def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
 
1327
        tree.lock_read()
 
1328
        limbodir = tempfile.mkdtemp(prefix='bzr-limbo-')
 
1329
        TreeTransformBase.__init__(self, tree, limbodir, pb, case_sensitive)
 
1330
 
 
1331
    def canonical_path(self, path):
 
1332
        return path
 
1333
 
 
1334
    def tree_kind(self, trans_id):
 
1335
        path = self._tree_id_paths.get(trans_id)
 
1336
        if path is None:
 
1337
            raise NoSuchFile(None)
 
1338
        file_id = self._tree.path2id(path)
 
1339
        return self._tree.kind(file_id)
 
1340
 
 
1341
    def _set_mode(self, trans_id, mode_id, typefunc):
 
1342
        """Set the mode of new file contents.
 
1343
        The mode_id is the existing file to get the mode from (often the same
 
1344
        as trans_id).  The operation is only performed if there's a mode match
 
1345
        according to typefunc.
 
1346
        """
 
1347
        # is it ok to ignore this?  probably
 
1348
        pass
 
1349
 
 
1350
    def iter_tree_children(self, parent_id):
 
1351
        """Iterate through the entry's tree children, if any"""
 
1352
        try:
 
1353
            path = self._tree_id_paths[parent_id]
 
1354
        except KeyError:
 
1355
            return
 
1356
        file_id = self.tree_file_id(parent_id)
 
1357
        for child in self._tree.inventory[file_id].children.iterkeys():
 
1358
            childpath = joinpath(path, child)
 
1359
            yield self.trans_id_tree_path(childpath)
 
1360
 
 
1361
 
 
1362
class _PreviewTree(object):
 
1363
    """Partial implementation of Tree to support show_diff_trees"""
 
1364
 
 
1365
    def __init__(self, transform):
 
1366
        self._transform = transform
 
1367
 
 
1368
    def lock_read(self):
 
1369
        # Perhaps in theory, this should lock the TreeTransform?
 
1370
        pass
 
1371
 
 
1372
    def unlock(self):
 
1373
        pass
 
1374
 
 
1375
    def iter_changes(self, from_tree, include_unchanged=False,
 
1376
                      specific_files=None, pb=None, extra_trees=None,
 
1377
                      require_versioned=True, want_unversioned=False):
 
1378
        """See InterTree.iter_changes.
 
1379
 
 
1380
        This implementation does not support include_unchanged, specific_files,
 
1381
        or want_unversioned.  extra_trees, require_versioned, and pb are
 
1382
        ignored.
 
1383
        """
 
1384
        if from_tree is not self._transform._tree:
 
1385
            raise ValueError('from_tree must be transform source tree.')
 
1386
        if include_unchanged:
 
1387
            raise ValueError('include_unchanged is not supported')
 
1388
        if specific_files is not None:
 
1389
            raise ValueError('specific_files is not supported')
 
1390
        if want_unversioned:
 
1391
            raise ValueError('want_unversioned is not supported')
 
1392
        return self._transform.iter_changes()
 
1393
 
 
1394
    def kind(self, file_id):
 
1395
        trans_id = self._transform.trans_id_file_id(file_id)
 
1396
        return self._transform.final_kind(trans_id)
 
1397
 
 
1398
    def get_file_mtime(self, file_id, path=None):
 
1399
        """See Tree.get_file_mtime"""
 
1400
        trans_id = self._transform.trans_id_file_id(file_id)
 
1401
        name = self._transform._limbo_name(trans_id)
 
1402
        return os.stat(name).st_mtime
 
1403
 
 
1404
    def get_file(self, file_id):
 
1405
        """See Tree.get_file"""
 
1406
        trans_id = self._transform.trans_id_file_id(file_id)
 
1407
        name = self._transform._limbo_name(trans_id)
 
1408
        return open(name, 'rb')
 
1409
 
 
1410
    def get_symlink_target(self, file_id):
 
1411
        """See Tree.get_symlink_target"""
 
1412
        trans_id = self._transform.trans_id_file_id(file_id)
 
1413
        name = self._transform._limbo_name(trans_id)
 
1414
        return os.readlink(name)
 
1415
 
 
1416
    def paths2ids(self, specific_files, trees=None, require_versioned=False):
 
1417
        """See Tree.paths2ids"""
 
1418
        return 'not_empty'
 
1419
 
1284
1420
 
1285
1421
def joinpath(parent, child):
1286
1422
    """Join tree-relative paths, handling the tree root specially"""
1325
1461
    return file_ids
1326
1462
 
1327
1463
 
1328
 
def build_tree(tree, wt, accelerator_tree=None):
 
1464
def build_tree(tree, wt, accelerator_tree=None, hardlink=False):
1329
1465
    """Create working tree for a branch, using a TreeTransform.
1330
1466
    
1331
1467
    This function should be used on empty trees, having a tree root at most.
1344
1480
    :param accelerator_tree: A tree which can be used for retrieving file
1345
1481
        contents more quickly than tree itself, i.e. a workingtree.  tree
1346
1482
        will be used for cases where accelerator_tree's content is different.
 
1483
    :param hardlink: If true, hard-link files to accelerator_tree, where
 
1484
        possible.  accelerator_tree must implement abspath, i.e. be a
 
1485
        working tree.
1347
1486
    """
1348
1487
    wt.lock_tree_write()
1349
1488
    try:
1352
1491
            if accelerator_tree is not None:
1353
1492
                accelerator_tree.lock_read()
1354
1493
            try:
1355
 
                return _build_tree(tree, wt, accelerator_tree)
 
1494
                return _build_tree(tree, wt, accelerator_tree, hardlink)
1356
1495
            finally:
1357
1496
                if accelerator_tree is not None:
1358
1497
                    accelerator_tree.unlock()
1362
1501
        wt.unlock()
1363
1502
 
1364
1503
 
1365
 
def _build_tree(tree, wt, accelerator_tree):
 
1504
def _build_tree(tree, wt, accelerator_tree, hardlink):
1366
1505
    """See build_tree."""
1367
 
    if len(wt.inventory) > 1:  # more than just a root
1368
 
        raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
 
1506
    for num, _unused in enumerate(wt.all_file_ids()):
 
1507
        if num > 0:  # more than just a root
 
1508
            raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1369
1509
    file_trans_id = {}
1370
1510
    top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1371
1511
    pp = ProgressPhase("Build phase", 2, top_pb)
1389
1529
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
1390
1530
        try:
1391
1531
            deferred_contents = []
 
1532
            num = 0
1392
1533
            for num, (tree_path, entry) in \
1393
1534
                enumerate(tree.inventory.iter_entries_by_dir()):
1394
1535
                pb.update("Building tree", num - len(deferred_contents),
1438
1579
                    new_trans_id = file_trans_id[file_id]
1439
1580
                    old_parent = tt.trans_id_tree_path(tree_path)
1440
1581
                    _reparent_children(tt, old_parent, new_trans_id)
1441
 
            for num, (trans_id, bytes) in enumerate(
1442
 
                _iter_files_bytes_accelerated(tree, accelerator_tree,
1443
 
                                              deferred_contents)):
1444
 
                tt.create_file(bytes, trans_id)
1445
 
                pb.update('Adding file contents',
1446
 
                          (num + len(tree.inventory) - len(deferred_contents)),
1447
 
                          len(tree.inventory))
 
1582
            offset = num + 1 - len(deferred_contents)
 
1583
            _create_files(tt, tree, deferred_contents, pb, offset,
 
1584
                          accelerator_tree, hardlink)
1448
1585
        finally:
1449
1586
            pb.finished()
1450
1587
        pp.next_phase()
1465
1602
    return result
1466
1603
 
1467
1604
 
1468
 
def _iter_files_bytes_accelerated(tree, accelerator_tree, desired_files):
 
1605
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
 
1606
                  hardlink):
 
1607
    total = len(desired_files) + offset
1469
1608
    if accelerator_tree is None:
1470
1609
        new_desired_files = desired_files
1471
1610
    else:
1472
 
        iter = accelerator_tree._iter_changes(tree, include_unchanged=True)
 
1611
        iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
1473
1612
        unchanged = dict((f, p[1]) for (f, p, c, v, d, n, k, e)
1474
 
                         in iter if not c)
 
1613
                         in iter if not (c or e[0] != e[1]))
1475
1614
        new_desired_files = []
1476
 
        for file_id, identifier in desired_files:
 
1615
        count = 0
 
1616
        for file_id, trans_id in desired_files:
1477
1617
            accelerator_path = unchanged.get(file_id)
1478
1618
            if accelerator_path is None:
1479
 
                new_desired_files.append((file_id, identifier))
 
1619
                new_desired_files.append((file_id, trans_id))
1480
1620
                continue
1481
 
            contents = accelerator_tree.get_file(file_id, accelerator_path)
1482
 
            try:
1483
 
                want_new = False
1484
 
                contents_bytes = (contents.read(),)
1485
 
            finally:
1486
 
                contents.close()
1487
 
            yield identifier, contents_bytes
1488
 
    for result in tree.iter_files_bytes(new_desired_files):
1489
 
        yield result
 
1621
            pb.update('Adding file contents', count + offset, total)
 
1622
            if hardlink:
 
1623
                tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
 
1624
                                   trans_id)
 
1625
            else:
 
1626
                contents = accelerator_tree.get_file(file_id, accelerator_path)
 
1627
                try:
 
1628
                    tt.create_file(contents, trans_id)
 
1629
                finally:
 
1630
                    contents.close()
 
1631
            count += 1
 
1632
        offset += count
 
1633
    for count, (trans_id, contents) in enumerate(tree.iter_files_bytes(
 
1634
                                                 new_desired_files)):
 
1635
        tt.create_file(contents, trans_id)
 
1636
        pb.update('Adding file contents', count + offset, total)
1490
1637
 
1491
1638
 
1492
1639
def _reparent_children(tt, old_parent, new_parent):
1709
1856
        if change_reporter:
1710
1857
            change_reporter = delta._ChangeReporter(
1711
1858
                unversioned_filter=working_tree.is_ignored)
1712
 
            delta.report_changes(tt._iter_changes(), change_reporter)
 
1859
            delta.report_changes(tt.iter_changes(), change_reporter)
1713
1860
        for conflict in conflicts:
1714
1861
            warning(conflict)
1715
1862
        pp.next_phase()
1725
1872
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
1726
1873
                 backups):
1727
1874
    merge_modified = working_tree.merge_modified()
1728
 
    change_list = target_tree._iter_changes(working_tree,
 
1875
    change_list = target_tree.iter_changes(working_tree,
1729
1876
        specific_files=specific_files, pb=pb)
1730
1877
    if target_tree.inventory.root is None:
1731
1878
        skip_root = True
1893
2040
            new_parent_id = tt.new_directory(parent_name + '.new',
1894
2041
                parent_parent, parent_file_id)
1895
2042
            _reparent_transform_children(tt, parent_id, new_parent_id)
1896
 
            tt.unversion_file(parent_id)
 
2043
            if parent_file_id is not None:
 
2044
                tt.unversion_file(parent_id)
1897
2045
            new_conflicts.add((c_type, 'Created directory', new_parent_id))
1898
2046
    return new_conflicts
1899
2047