/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: Canonical.com Patch Queue Manager
  • Date: 2007-02-15 18:14:16 UTC
  • mfrom: (2290.1.1 jam-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20070215181416-864dbe690a0f3da8
(John Arbash Meinel) make 'bzr remove-tree' a non-hidden command.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
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
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
18
import errno
19
19
from stat import S_ISREG
20
20
 
 
21
from bzrlib import bzrdir, errors
 
22
from bzrlib.lazy_import import lazy_import
 
23
lazy_import(globals(), """
 
24
from bzrlib import delta
 
25
""")
21
26
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
22
27
                           ReusingTransform, NotVersionedError, CantMoveRoot,
23
 
                           ExistingLimbo, ImmortalLimbo)
 
28
                           ExistingLimbo, ImmortalLimbo, NoFinalPath)
24
29
from bzrlib.inventory import InventoryEntry
25
30
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
26
31
                            delete_any)
27
32
from bzrlib.progress import DummyProgress, ProgressPhase
28
33
from bzrlib.trace import mutter, warning
 
34
from bzrlib import tree
29
35
import bzrlib.ui 
 
36
import bzrlib.urlutils as urlutils
30
37
 
31
38
 
32
39
ROOT_PARENT = "root-parent"
70
77
     * set_executability
71
78
    """
72
79
    def __init__(self, tree, pb=DummyProgress()):
73
 
        """Note: a write lock is taken on the tree.
 
80
        """Note: a tree_write lock is taken on the tree.
74
81
        
75
82
        Use TreeTransform.finalize() to release the lock
76
83
        """
77
84
        object.__init__(self)
78
85
        self._tree = tree
79
 
        self._tree.lock_write()
 
86
        self._tree.lock_tree_write()
80
87
        try:
81
88
            control_files = self._tree._control_files
82
 
            self._limbodir = control_files.controlfilename('limbo')
 
89
            self._limbodir = urlutils.local_path_from_url(
 
90
                control_files.controlfilename('limbo'))
83
91
            try:
84
92
                os.mkdir(self._limbodir)
85
93
            except OSError, e:
101
109
        self._removed_id = set()
102
110
        self._tree_path_ids = {}
103
111
        self._tree_id_paths = {}
 
112
        self._realpaths = {}
 
113
        # Cache of realpath results, to speed up canonical_path
 
114
        self._relpaths = {}
 
115
        # Cache of relpath results, to speed up canonical_path
104
116
        self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
105
117
        self.__done = False
106
118
        self._pb = pb
211
223
    def canonical_path(self, path):
212
224
        """Get the canonical tree-relative path"""
213
225
        # don't follow final symlinks
214
 
        dirname, basename = os.path.split(self._tree.abspath(path))
215
 
        dirname = os.path.realpath(dirname)
216
 
        return self._tree.relpath(pathjoin(dirname, basename))
 
226
        abs = self._tree.abspath(path)
 
227
        if abs in self._relpaths:
 
228
            return self._relpaths[abs]
 
229
        dirname, basename = os.path.split(abs)
 
230
        if dirname not in self._realpaths:
 
231
            self._realpaths[dirname] = os.path.realpath(dirname)
 
232
        dirname = self._realpaths[dirname]
 
233
        abs = pathjoin(dirname, basename)
 
234
        if dirname in self._relpaths:
 
235
            relpath = pathjoin(self._relpaths[dirname], basename)
 
236
            relpath = relpath.rstrip('/\\')
 
237
        else:
 
238
            relpath = self._tree.relpath(abs)
 
239
        self._relpaths[abs] = relpath
 
240
        return relpath
217
241
 
218
242
    def trans_id_tree_path(self, path):
219
243
        """Determine (and maybe set) the transaction ID for a tree path."""
241
265
        New file takes the permissions of any existing file with that id,
242
266
        unless mode_id is specified.
243
267
        """
244
 
        f = file(self._limbo_name(trans_id), 'wb')
245
 
        unique_add(self._new_contents, trans_id, 'file')
246
 
        for segment in contents:
247
 
            f.write(segment)
248
 
        f.close()
 
268
        name = self._limbo_name(trans_id)
 
269
        f = open(name, 'wb')
 
270
        try:
 
271
            try:
 
272
                unique_add(self._new_contents, trans_id, 'file')
 
273
            except:
 
274
                # Clean up the file, it never got registered so
 
275
                # TreeTransform.finalize() won't clean it up.
 
276
                f.close()
 
277
                os.unlink(name)
 
278
                raise
 
279
 
 
280
            f.writelines(contents)
 
281
        finally:
 
282
            f.close()
249
283
        self._set_mode(trans_id, mode_id, S_ISREG)
250
284
 
251
285
    def _set_mode(self, trans_id, mode_id, typefunc):
261
295
        except KeyError:
262
296
            return
263
297
        try:
264
 
            mode = os.stat(old_path).st_mode
 
298
            mode = os.stat(self._tree.abspath(old_path)).st_mode
265
299
        except OSError, e:
266
300
            if e.errno == errno.ENOENT:
267
301
                return
426
460
        try:
427
461
            return self._new_name[trans_id]
428
462
        except KeyError:
429
 
            return os.path.basename(self._tree_id_paths[trans_id])
 
463
            try:
 
464
                return os.path.basename(self._tree_id_paths[trans_id])
 
465
            except KeyError:
 
466
                raise NoFinalPath(trans_id, self)
430
467
 
431
468
    def by_parent(self):
432
469
        """Return a map of parent: children for known parents.
445
482
 
446
483
    def path_changed(self, trans_id):
447
484
        """Return True if a trans_id's path has changed."""
448
 
        return trans_id in self._new_name or trans_id in self._new_parent
 
485
        return (trans_id in self._new_name) or (trans_id in self._new_parent)
 
486
 
 
487
    def new_contents(self, trans_id):
 
488
        return (trans_id in self._new_contents)
449
489
 
450
490
    def find_conflicts(self):
451
491
        """Find any violations of inventory or filesystem invariants"""
477
517
                        self.tree_kind(t) == 'directory'])
478
518
        for trans_id in self._removed_id:
479
519
            file_id = self.tree_file_id(trans_id)
480
 
            if self._tree.inventory[file_id].kind in ('directory', 
481
 
                                                      'root_directory'):
 
520
            if self._tree.inventory[file_id].kind == 'directory':
482
521
                parents.append(trans_id)
483
522
 
484
523
        for parent_id in parents:
521
560
        if child_id is None:
522
561
            return lexists(self._tree.abspath(childpath))
523
562
        else:
524
 
            if tt.final_parent(child_id) != parent_id:
 
563
            if self.final_parent(child_id) != parent_id:
525
564
                return False
526
 
            if child_id in tt._removed_contents:
 
565
            if child_id in self._removed_contents:
527
566
                # XXX What about dangling file-ids?
528
567
                return False
529
568
            else:
537
576
            parent_id = trans_id
538
577
            while parent_id is not ROOT_PARENT:
539
578
                seen.add(parent_id)
540
 
                parent_id = self.final_parent(parent_id)
 
579
                try:
 
580
                    parent_id = self.final_parent(parent_id)
 
581
                except KeyError:
 
582
                    break
541
583
                if parent_id == trans_id:
542
584
                    conflicts.append(('parent loop', trans_id))
543
585
                if parent_id in seen:
617
659
            last_name = None
618
660
            last_trans_id = None
619
661
            for name, trans_id in name_ids:
 
662
                try:
 
663
                    kind = self.final_kind(trans_id)
 
664
                except NoSuchFile:
 
665
                    kind = None
 
666
                file_id = self.final_file_id(trans_id)
 
667
                if kind is None and file_id is None:
 
668
                    continue
620
669
                if name == last_name:
621
670
                    conflicts.append(('duplicate', last_trans_id, trans_id,
622
671
                    name))
623
 
                try:
624
 
                    kind = self.final_kind(trans_id)
625
 
                except NoSuchFile:
626
 
                    kind = None
627
 
                file_id = self.final_file_id(trans_id)
628
 
                if kind is not None or file_id is not None:
629
 
                    last_name = name
630
 
                    last_trans_id = trans_id
 
672
                last_name = name
 
673
                last_trans_id = trans_id
631
674
        return conflicts
632
675
 
633
676
    def _duplicate_ids(self):
829
872
        parent_id is the transaction id of the parent directory of the file.
830
873
        contents is an iterator of bytestrings, which will be used to produce
831
874
        the file.
832
 
        file_id is the inventory ID of the file, if it is to be versioned.
 
875
        :param file_id: The inventory ID of the file, if it is to be versioned.
 
876
        :param executable: Only valid when a file_id has been supplied.
833
877
        """
834
878
        trans_id = self._new_entry(name, parent_id, file_id)
 
879
        # TODO: rather than scheduling a set_executable call,
 
880
        # have create_file create the file with the right mode.
835
881
        self.create_file(contents, trans_id)
836
882
        if executable is not None:
837
883
            self.set_executability(executable, trans_id)
861
907
        self.create_symlink(target, trans_id)
862
908
        return trans_id
863
909
 
 
910
    def _affected_ids(self):
 
911
        """Return the set of transform ids affected by the transform"""
 
912
        trans_ids = set(self._removed_id)
 
913
        trans_ids.update(self._new_id.keys())
 
914
        trans_ids.update(self._removed_contents)
 
915
        trans_ids.update(self._new_contents.keys())
 
916
        trans_ids.update(self._new_executability.keys())
 
917
        trans_ids.update(self._new_name.keys())
 
918
        trans_ids.update(self._new_parent.keys())
 
919
        return trans_ids
 
920
 
 
921
    def _get_file_id_maps(self):
 
922
        """Return mapping of file_ids to trans_ids in the to and from states"""
 
923
        trans_ids = self._affected_ids()
 
924
        from_trans_ids = {}
 
925
        to_trans_ids = {}
 
926
        # Build up two dicts: trans_ids associated with file ids in the
 
927
        # FROM state, vs the TO state.
 
928
        for trans_id in trans_ids:
 
929
            from_file_id = self.tree_file_id(trans_id)
 
930
            if from_file_id is not None:
 
931
                from_trans_ids[from_file_id] = trans_id
 
932
            to_file_id = self.final_file_id(trans_id)
 
933
            if to_file_id is not None:
 
934
                to_trans_ids[to_file_id] = trans_id
 
935
        return from_trans_ids, to_trans_ids
 
936
 
 
937
    def _from_file_data(self, from_trans_id, from_versioned, file_id):
 
938
        """Get data about a file in the from (tree) state
 
939
 
 
940
        Return a (name, parent, kind, executable) tuple
 
941
        """
 
942
        from_path = self._tree_id_paths.get(from_trans_id)
 
943
        if from_versioned:
 
944
            # get data from working tree if versioned
 
945
            from_entry = self._tree.inventory[file_id]
 
946
            from_name = from_entry.name
 
947
            from_parent = from_entry.parent_id
 
948
        else:
 
949
            from_entry = None
 
950
            if from_path is None:
 
951
                # File does not exist in FROM state
 
952
                from_name = None
 
953
                from_parent = None
 
954
            else:
 
955
                # File exists, but is not versioned.  Have to use path-
 
956
                # splitting stuff
 
957
                from_name = os.path.basename(from_path)
 
958
                tree_parent = self.get_tree_parent(from_trans_id)
 
959
                from_parent = self.tree_file_id(tree_parent)
 
960
        if from_path is not None:
 
961
            from_kind, from_executable, from_stats = \
 
962
                self._tree._comparison_data(from_entry, from_path)
 
963
        else:
 
964
            from_kind = None
 
965
            from_executable = False
 
966
        return from_name, from_parent, from_kind, from_executable
 
967
 
 
968
    def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
 
969
        """Get data about a file in the to (target) state
 
970
 
 
971
        Return a (name, parent, kind, executable) tuple
 
972
        """
 
973
        to_name = self.final_name(to_trans_id)
 
974
        try:
 
975
            to_kind = self.final_kind(to_trans_id)
 
976
        except NoSuchFile:
 
977
            to_kind = None
 
978
        to_parent = self.final_file_id(self.final_parent(to_trans_id))
 
979
        if to_trans_id in self._new_executability:
 
980
            to_executable = self._new_executability[to_trans_id]
 
981
        elif to_trans_id == from_trans_id:
 
982
            to_executable = from_executable
 
983
        else:
 
984
            to_executable = False
 
985
        return to_name, to_parent, to_kind, to_executable
 
986
 
 
987
    def _iter_changes(self):
 
988
        """Produce output in the same format as Tree._iter_changes.
 
989
 
 
990
        Will produce nonsensical results if invoked while inventory/filesystem
 
991
        conflicts (as reported by TreeTransform.find_conflicts()) are present.
 
992
 
 
993
        This reads the Transform, but only reproduces changes involving a
 
994
        file_id.  Files that are not versioned in either of the FROM or TO
 
995
        states are not reflected.
 
996
        """
 
997
        final_paths = FinalPaths(self)
 
998
        from_trans_ids, to_trans_ids = self._get_file_id_maps()
 
999
        results = []
 
1000
        # Now iterate through all active file_ids
 
1001
        for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
 
1002
            modified = False
 
1003
            from_trans_id = from_trans_ids.get(file_id)
 
1004
            # find file ids, and determine versioning state
 
1005
            if from_trans_id is None:
 
1006
                from_versioned = False
 
1007
                from_trans_id = to_trans_ids[file_id]
 
1008
            else:
 
1009
                from_versioned = True
 
1010
            to_trans_id = to_trans_ids.get(file_id)
 
1011
            if to_trans_id is None:
 
1012
                to_versioned = False
 
1013
                to_trans_id = from_trans_id
 
1014
            else:
 
1015
                to_versioned = True
 
1016
 
 
1017
            from_name, from_parent, from_kind, from_executable = \
 
1018
                self._from_file_data(from_trans_id, from_versioned, file_id)
 
1019
 
 
1020
            to_name, to_parent, to_kind, to_executable = \
 
1021
                self._to_file_data(to_trans_id, from_trans_id, from_executable)
 
1022
 
 
1023
            to_path = final_paths.get_path(to_trans_id)
 
1024
            if from_kind != to_kind:
 
1025
                modified = True
 
1026
            elif to_kind in ('file' or 'symlink') and (
 
1027
                to_trans_id != from_trans_id or
 
1028
                to_trans_id in self._new_contents):
 
1029
                modified = True
 
1030
            if (not modified and from_versioned == to_versioned and
 
1031
                from_parent==to_parent and from_name == to_name and
 
1032
                from_executable == to_executable):
 
1033
                continue
 
1034
            results.append((file_id, to_path, modified,
 
1035
                   (from_versioned, to_versioned),
 
1036
                   (from_parent, to_parent),
 
1037
                   (from_name, to_name),
 
1038
                   (from_kind, to_kind),
 
1039
                   (from_executable, to_executable)))
 
1040
        return iter(sorted(results, key=lambda x:x[1]))
 
1041
 
 
1042
 
864
1043
def joinpath(parent, child):
865
1044
    """Join tree-relative paths, handling the tree root specially"""
866
1045
    if parent is None or parent == "":
902
1081
    file_ids.sort(key=tree.id2path)
903
1082
    return file_ids
904
1083
 
 
1084
 
905
1085
def build_tree(tree, wt):
906
 
    """Create working tree for a branch, using a Transaction."""
 
1086
    """Create working tree for a branch, using a TreeTransform.
 
1087
    
 
1088
    This function should be used on empty trees, having a tree root at most.
 
1089
    (see merge and revert functionality for working with existing trees)
 
1090
 
 
1091
    Existing files are handled like so:
 
1092
    
 
1093
    - Existing bzrdirs take precedence over creating new items.  They are
 
1094
      created as '%s.diverted' % name.
 
1095
    - Otherwise, if the content on disk matches the content we are building,
 
1096
      it is silently replaced.
 
1097
    - Otherwise, conflict resolution will move the old file to 'oldname.moved'.
 
1098
    """
 
1099
    if len(wt.inventory) > 1:  # more than just a root
 
1100
        raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
907
1101
    file_trans_id = {}
908
1102
    top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
909
1103
    pp = ProgressPhase("Build phase", 2, top_pb)
 
1104
    if tree.inventory.root is not None:
 
1105
        wt.set_root_id(tree.inventory.root.file_id)
910
1106
    tt = TreeTransform(wt)
 
1107
    divert = set()
911
1108
    try:
912
1109
        pp.next_phase()
913
 
        file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
914
 
        file_ids = topology_sorted_ids(tree)
 
1110
        file_trans_id[wt.get_root_id()] = \
 
1111
            tt.trans_id_tree_file_id(wt.get_root_id())
915
1112
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
916
1113
        try:
917
 
            for num, file_id in enumerate(file_ids):
918
 
                pb.update("Building tree", num, len(file_ids))
919
 
                entry = tree.inventory[file_id]
 
1114
            for num, (tree_path, entry) in \
 
1115
                enumerate(tree.inventory.iter_entries_by_dir()):
 
1116
                pb.update("Building tree", num, len(tree.inventory))
920
1117
                if entry.parent_id is None:
921
1118
                    continue
 
1119
                reparent = False
 
1120
                file_id = entry.file_id
 
1121
                target_path = wt.abspath(tree_path)
 
1122
                try:
 
1123
                    kind = file_kind(target_path)
 
1124
                except NoSuchFile:
 
1125
                    pass
 
1126
                else:
 
1127
                    if kind == "directory":
 
1128
                        try:
 
1129
                            bzrdir.BzrDir.open(target_path)
 
1130
                        except errors.NotBranchError:
 
1131
                            pass
 
1132
                        else:
 
1133
                            divert.add(file_id)
 
1134
                    if (file_id not in divert and
 
1135
                        _content_match(tree, entry, file_id, kind,
 
1136
                        target_path)):
 
1137
                        tt.delete_contents(tt.trans_id_tree_path(tree_path))
 
1138
                        if kind == 'directory':
 
1139
                            reparent = True
922
1140
                if entry.parent_id not in file_trans_id:
923
1141
                    raise repr(entry.parent_id)
924
1142
                parent_id = file_trans_id[entry.parent_id]
925
 
                file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, 
 
1143
                file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
926
1144
                                                      tree)
 
1145
                if reparent:
 
1146
                    new_trans_id = file_trans_id[file_id]
 
1147
                    old_parent = tt.trans_id_tree_path(tree_path)
 
1148
                    _reparent_children(tt, old_parent, new_trans_id)
927
1149
        finally:
928
1150
            pb.finished()
929
1151
        pp.next_phase()
 
1152
        divert_trans = set(file_trans_id[f] for f in divert)
 
1153
        resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
 
1154
        raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
 
1155
        conflicts = cook_conflicts(raw_conflicts, tt)
 
1156
        for conflict in conflicts:
 
1157
            warning(conflict)
 
1158
        try:
 
1159
            wt.add_conflicts(conflicts)
 
1160
        except errors.UnsupportedOperation:
 
1161
            pass
930
1162
        tt.apply()
931
1163
    finally:
932
1164
        tt.finalize()
933
1165
        top_pb.finished()
934
1166
 
 
1167
 
 
1168
def _reparent_children(tt, old_parent, new_parent):
 
1169
    for child in tt.iter_tree_children(old_parent):
 
1170
        tt.adjust_path(tt.final_name(child), new_parent, child)
 
1171
 
 
1172
 
 
1173
def _content_match(tree, entry, file_id, kind, target_path):
 
1174
    if entry.kind != kind:
 
1175
        return False
 
1176
    if entry.kind == "directory":
 
1177
        return True
 
1178
    if entry.kind == "file":
 
1179
        if tree.get_file(file_id).read() == file(target_path, 'rb').read():
 
1180
            return True
 
1181
    elif entry.kind == "symlink":
 
1182
        if tree.get_symlink_target(file_id) == os.readlink(target_path):
 
1183
            return True
 
1184
    return False
 
1185
 
 
1186
 
 
1187
def resolve_checkout(tt, conflicts, divert):
 
1188
    new_conflicts = set()
 
1189
    for c_type, conflict in ((c[0], c) for c in conflicts):
 
1190
        # Anything but a 'duplicate' would indicate programmer error
 
1191
        assert c_type == 'duplicate', c_type
 
1192
        # Now figure out which is new and which is old
 
1193
        if tt.new_contents(conflict[1]):
 
1194
            new_file = conflict[1]
 
1195
            old_file = conflict[2]
 
1196
        else:
 
1197
            new_file = conflict[2]
 
1198
            old_file = conflict[1]
 
1199
 
 
1200
        # We should only get here if the conflict wasn't completely
 
1201
        # resolved
 
1202
        final_parent = tt.final_parent(old_file)
 
1203
        if new_file in divert:
 
1204
            new_name = tt.final_name(old_file)+'.diverted'
 
1205
            tt.adjust_path(new_name, final_parent, new_file)
 
1206
            new_conflicts.add((c_type, 'Diverted to',
 
1207
                               new_file, old_file))
 
1208
        else:
 
1209
            new_name = tt.final_name(old_file)+'.moved'
 
1210
            tt.adjust_path(new_name, final_parent, old_file)
 
1211
            new_conflicts.add((c_type, 'Moved existing file to',
 
1212
                               old_file, new_file))
 
1213
    return new_conflicts
 
1214
 
 
1215
 
935
1216
def new_by_entry(tt, entry, parent_id, tree):
936
1217
    """Create a new file according to its inventory entry"""
937
1218
    name = entry.name
950
1231
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
951
1232
    """Create new file contents according to an inventory entry."""
952
1233
    if entry.kind == "file":
953
 
        if lines == None:
 
1234
        if lines is None:
954
1235
            lines = tree.get_file(entry.file_id).readlines()
955
1236
        tt.create_file(lines, trans_id, mode_id=mode_id)
956
1237
    elif entry.kind == "symlink":
966
1247
 
967
1248
def find_interesting(working_tree, target_tree, filenames):
968
1249
    """Find the ids corresponding to specified filenames."""
969
 
    if not filenames:
970
 
        interesting_ids = None
971
 
    else:
972
 
        interesting_ids = set()
973
 
        for tree_path in filenames:
974
 
            not_found = True
975
 
            for tree in (working_tree, target_tree):
976
 
                file_id = tree.inventory.path2id(tree_path)
977
 
                if file_id is not None:
978
 
                    interesting_ids.add(file_id)
979
 
                    not_found = False
980
 
            if not_found:
981
 
                raise NotVersionedError(path=tree_path)
982
 
    return interesting_ids
 
1250
    trees = (working_tree, target_tree)
 
1251
    return tree.find_ids_across_trees(filenames, trees)
983
1252
 
984
1253
 
985
1254
def change_entry(tt, file_id, working_tree, target_tree, 
1023
1292
 
1024
1293
 
1025
1294
def get_backup_name(entry, by_parent, parent_trans_id, tt):
 
1295
    return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
 
1296
 
 
1297
 
 
1298
def _get_backup_name(name, by_parent, parent_trans_id, tt):
1026
1299
    """Produce a backup-style name that appears to be available"""
1027
1300
    def name_gen():
1028
1301
        counter = 1
1029
1302
        while True:
1030
 
            yield "%s.~%d~" % (entry.name, counter)
 
1303
            yield "%s.~%d~" % (name, counter)
1031
1304
            counter += 1
1032
 
    for name in name_gen():
1033
 
        if not tt.has_named_child(by_parent, parent_trans_id, name):
1034
 
            return name
 
1305
    for new_name in name_gen():
 
1306
        if not tt.has_named_child(by_parent, parent_trans_id, new_name):
 
1307
            return new_name
 
1308
 
1035
1309
 
1036
1310
def _entry_changes(file_id, entry, working_tree):
1037
1311
    """Determine in which ways the inventory entry has changed.
1045
1319
    try:
1046
1320
        working_kind = working_tree.kind(file_id)
1047
1321
        has_contents = True
1048
 
    except OSError, e:
1049
 
        if e.errno != errno.ENOENT:
1050
 
            raise
 
1322
    except NoSuchFile:
1051
1323
        has_contents = False
1052
1324
        contents_mod = True
1053
1325
        meta_mod = False
1054
1326
    if has_contents is True:
1055
 
        real_e_kind = entry.kind
1056
 
        if real_e_kind == 'root_directory':
1057
 
            real_e_kind = 'directory'
1058
 
        if real_e_kind != working_kind:
 
1327
        if entry.kind != working_kind:
1059
1328
            contents_mod, meta_mod = True, False
1060
1329
        else:
1061
1330
            cur_entry._read_tree_state(working_tree.id2path(file_id), 
1066
1335
 
1067
1336
 
1068
1337
def revert(working_tree, target_tree, filenames, backups=False, 
1069
 
           pb=DummyProgress()):
 
1338
           pb=DummyProgress(), change_reporter=None):
1070
1339
    """Revert a working tree's contents to those of a target tree."""
1071
1340
    interesting_ids = find_interesting(working_tree, target_tree, filenames)
1072
 
    def interesting(file_id):
1073
 
        return interesting_ids is None or file_id in interesting_ids
1074
 
 
1075
1341
    tt = TreeTransform(working_tree, pb)
1076
1342
    try:
1077
 
        merge_modified = working_tree.merge_modified()
1078
 
        trans_id = {}
1079
 
        def trans_id_file_id(file_id):
1080
 
            try:
1081
 
                return trans_id[file_id]
1082
 
            except KeyError:
1083
 
                return tt.trans_id_tree_file_id(file_id)
1084
 
 
1085
 
        pp = ProgressPhase("Revert phase", 4, pb)
1086
 
        pp.next_phase()
1087
 
        sorted_interesting = [i for i in topology_sorted_ids(target_tree) if
1088
 
                              interesting(i)]
1089
 
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1090
 
        try:
1091
 
            by_parent = tt.by_parent()
1092
 
            for id_num, file_id in enumerate(sorted_interesting):
1093
 
                child_pb.update("Reverting file", id_num+1, 
1094
 
                                len(sorted_interesting))
1095
 
                if file_id not in working_tree.inventory:
1096
 
                    entry = target_tree.inventory[file_id]
1097
 
                    parent_id = trans_id_file_id(entry.parent_id)
1098
 
                    e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
1099
 
                    trans_id[file_id] = e_trans_id
1100
 
                else:
1101
 
                    backup_this = backups
1102
 
                    if file_id in merge_modified:
1103
 
                        backup_this = False
1104
 
                        del merge_modified[file_id]
1105
 
                    change_entry(tt, file_id, working_tree, target_tree, 
1106
 
                                 trans_id_file_id, backup_this, trans_id,
1107
 
                                 by_parent)
1108
 
        finally:
1109
 
            child_pb.finished()
1110
 
        pp.next_phase()
1111
 
        wt_interesting = [i for i in working_tree.inventory if interesting(i)]
1112
 
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1113
 
        try:
1114
 
            for id_num, file_id in enumerate(wt_interesting):
1115
 
                child_pb.update("New file check", id_num+1, 
1116
 
                                len(sorted_interesting))
1117
 
                if file_id not in target_tree:
1118
 
                    trans_id = tt.trans_id_tree_file_id(file_id)
1119
 
                    tt.unversion_file(trans_id)
1120
 
                    if file_id in merge_modified:
1121
 
                        tt.delete_contents(trans_id)
1122
 
                        del merge_modified[file_id]
 
1343
        pp = ProgressPhase("Revert phase", 3, pb)
 
1344
        pp.next_phase()
 
1345
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1346
        try:
 
1347
            _alter_files(working_tree, target_tree, tt, child_pb, 
 
1348
                         interesting_ids, backups)
1123
1349
        finally:
1124
1350
            child_pb.finished()
1125
1351
        pp.next_phase()
1129
1355
        finally:
1130
1356
            child_pb.finished()
1131
1357
        conflicts = cook_conflicts(raw_conflicts, tt)
 
1358
        if change_reporter:
 
1359
            change_reporter = delta.ChangeReporter(working_tree.inventory)
 
1360
            delta.report_changes(tt._iter_changes(), change_reporter)
1132
1361
        for conflict in conflicts:
1133
1362
            warning(conflict)
1134
1363
        pp.next_phase()
1140
1369
    return conflicts
1141
1370
 
1142
1371
 
1143
 
def resolve_conflicts(tt, pb=DummyProgress()):
 
1372
def _alter_files(working_tree, target_tree, tt, pb, interesting_ids,
 
1373
                 backups):
 
1374
    merge_modified = working_tree.merge_modified()
 
1375
    change_list = target_tree._iter_changes(working_tree,
 
1376
        specific_file_ids=interesting_ids, pb=pb)
 
1377
    if target_tree.inventory.root is None:
 
1378
        skip_root = True
 
1379
    else:
 
1380
        skip_root = False
 
1381
    basis_tree = None
 
1382
    for id_num, (file_id, path, changed_content, versioned, parent, name,
 
1383
        kind, executable) in enumerate(change_list):
 
1384
        if skip_root and file_id[0] is not None and parent[0] is None:
 
1385
            continue
 
1386
        trans_id = tt.trans_id_file_id(file_id)
 
1387
        mode_id = None
 
1388
        if changed_content:
 
1389
            keep_content = False
 
1390
            if kind[0] == 'file' and (backups or kind[1] is None):
 
1391
                wt_sha1 = working_tree.get_file_sha1(file_id)
 
1392
                if merge_modified.get(file_id) != wt_sha1:
 
1393
                    if basis_tree is None:
 
1394
                        basis_tree = working_tree.basis_tree()
 
1395
                    if file_id in basis_tree:
 
1396
                        if wt_sha1 != basis_tree.get_file_sha1(file_id):
 
1397
                            keep_content = True
 
1398
                    elif kind[1] is None and not versioned[1]:
 
1399
                        keep_content = True
 
1400
            if kind[0] is not None:
 
1401
                if not keep_content:
 
1402
                    tt.delete_contents(trans_id)
 
1403
                elif kind[1] is not None:
 
1404
                    parent_trans_id = tt.trans_id_file_id(parent[0])
 
1405
                    by_parent = tt.by_parent()
 
1406
                    backup_name = _get_backup_name(name[0], by_parent,
 
1407
                                                   parent_trans_id, tt)
 
1408
                    tt.adjust_path(backup_name, parent_trans_id, trans_id)
 
1409
                    new_trans_id = tt.create_path(name[0], parent_trans_id)
 
1410
                    if versioned == (True, True):
 
1411
                        tt.unversion_file(trans_id)
 
1412
                        tt.version_file(file_id, new_trans_id)
 
1413
                    # New contents should have the same unix perms as old
 
1414
                    # contents
 
1415
                    mode_id = trans_id
 
1416
                    trans_id = new_trans_id
 
1417
            if kind[1] == 'directory':
 
1418
                tt.create_directory(trans_id)
 
1419
            elif kind[1] == 'symlink':
 
1420
                tt.create_symlink(target_tree.get_symlink_target(file_id),
 
1421
                                  trans_id)
 
1422
            elif kind[1] == 'file':
 
1423
                tt.create_file(target_tree.get_file_lines(file_id),
 
1424
                               trans_id, mode_id)
 
1425
                # preserve the execute bit when backing up
 
1426
                if keep_content and executable[0] == executable[1]:
 
1427
                    tt.set_executability(executable[1], trans_id)
 
1428
            else:
 
1429
                assert kind[1] is None
 
1430
        if versioned == (False, True):
 
1431
            tt.version_file(file_id, trans_id)
 
1432
        if versioned == (True, False):
 
1433
            tt.unversion_file(trans_id)
 
1434
        if (name[1] is not None and 
 
1435
            (name[0] != name[1] or parent[0] != parent[1])):
 
1436
            tt.adjust_path(name[1], tt.trans_id_file_id(parent[1]), trans_id)
 
1437
        if executable[0] != executable[1] and kind[1] == "file":
 
1438
            tt.set_executability(executable[1], trans_id)
 
1439
 
 
1440
 
 
1441
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
1144
1442
    """Make many conflict-resolution attempts, but die if they fail"""
 
1443
    if pass_func is None:
 
1444
        pass_func = conflict_pass
1145
1445
    new_conflicts = set()
1146
1446
    try:
1147
1447
        for n in range(10):
1149
1449
            conflicts = tt.find_conflicts()
1150
1450
            if len(conflicts) == 0:
1151
1451
                return new_conflicts
1152
 
            new_conflicts.update(conflict_pass(tt, conflicts))
 
1452
            new_conflicts.update(pass_func(tt, conflicts))
1153
1453
        raise MalformedTransform(conflicts=conflicts)
1154
1454
    finally:
1155
1455
        pb.clear()
1188
1488
            trans_id = conflict[1]
1189
1489
            try:
1190
1490
                tt.cancel_deletion(trans_id)
1191
 
                new_conflicts.add((c_type, 'Not deleting', trans_id))
 
1491
                new_conflicts.add(('deleting parent', 'Not deleting', 
 
1492
                                   trans_id))
1192
1493
            except KeyError:
1193
1494
                tt.create_directory(trans_id)
1194
 
                new_conflicts.add((c_type, 'Created directory.', trans_id))
 
1495
                new_conflicts.add((c_type, 'Created directory', trans_id))
1195
1496
        elif c_type == 'unversioned parent':
1196
1497
            tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1197
1498
            new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1198
1499
    return new_conflicts
1199
1500
 
 
1501
 
1200
1502
def cook_conflicts(raw_conflicts, tt):
1201
1503
    """Generate a list of cooked conflicts, sorted by file path"""
1202
 
    def key(conflict):
1203
 
        if conflict.path is not None:
1204
 
            return conflict.path, conflict.typestring
1205
 
        elif getattr(conflict, "conflict_path", None) is not None:
1206
 
            return conflict.conflict_path, conflict.typestring
1207
 
        else:
1208
 
            return None, conflict.typestring
 
1504
    from bzrlib.conflicts import Conflict
 
1505
    conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
 
1506
    return sorted(conflict_iter, key=Conflict.sort_key)
1209
1507
 
1210
 
    return sorted(list(iter_cook_conflicts(raw_conflicts, tt)), key=key)
1211
1508
 
1212
1509
def iter_cook_conflicts(raw_conflicts, tt):
1213
1510
    from bzrlib.conflicts import Conflict