~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transform.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2008-01-03 18:09:01 UTC
  • mfrom: (3159.1.1 trunk)
  • Revision ID: pqm@pqm.ubuntu.com-20080103180901-w987y1ftqoh02qbm
(vila) Fix #179368 by keeping the current range hint on
        ShortReadvErrors

Show diffs side-by-side

added added

removed removed

Lines of Context:
29
29
""")
30
30
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
31
31
                           ReusingTransform, NotVersionedError, CantMoveRoot,
32
 
                           ExistingLimbo, ImmortalLimbo, NoFinalPath)
 
32
                           ExistingLimbo, ImmortalLimbo, NoFinalPath,
 
33
                           UnableCreateSymlink)
33
34
from bzrlib.inventory import InventoryEntry
34
35
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
35
 
                            delete_any)
 
36
                            delete_any, has_symlinks)
36
37
from bzrlib.progress import DummyProgress, ProgressPhase
37
 
from bzrlib.symbol_versioning import deprecated_function, zero_fifteen
 
38
from bzrlib.symbol_versioning import (
 
39
        deprecated_function,
 
40
        zero_fifteen,
 
41
        zero_ninety,
 
42
        )
38
43
from bzrlib.trace import mutter, warning
39
44
from bzrlib import tree
40
45
import bzrlib.ui
85
90
     * create_file or create_directory or create_symlink
86
91
     * version_file
87
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.  
88
129
    """
89
130
    def __init__(self, tree, pb=DummyProgress()):
90
131
        """Note: a tree_write lock is taken on the tree.
104
145
            except OSError, e:
105
146
                if e.errno == errno.EEXIST:
106
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
 
107
156
        except: 
108
157
            self._tree.unlock()
109
158
            raise
110
159
 
 
160
        # counter used to generate trans-ids (which are locally unique)
111
161
        self._id_number = 0
 
162
        # mapping of trans_id -> new basename
112
163
        self._new_name = {}
 
164
        # mapping of trans_id -> new parent trans_id
113
165
        self._new_parent = {}
 
166
        # mapping of trans_id with new contents -> new file_kind
114
167
        self._new_contents = {}
115
168
        # A mapping of transform ids to their limbo filename
116
169
        self._limbo_files = {}
121
174
        self._limbo_children_names = {}
122
175
        # List of transform ids that need to be renamed from limbo into place
123
176
        self._needs_rename = set()
 
177
        # Set of trans_ids whose contents will be removed
124
178
        self._removed_contents = set()
 
179
        # Mapping of trans_id -> new execute-bit value
125
180
        self._new_executability = {}
 
181
        # Mapping of trans_id -> new tree-reference value
126
182
        self._new_reference_revision = {}
 
183
        # Mapping of trans_id -> new file_id
127
184
        self._new_id = {}
 
185
        # Mapping of old file-id -> trans_id
128
186
        self._non_present_ids = {}
 
187
        # Mapping of new file_id -> trans_id
129
188
        self._r_new_id = {}
 
189
        # Set of file_ids that will be removed
130
190
        self._removed_id = set()
 
191
        # Mapping of path in old tree -> trans_id
131
192
        self._tree_path_ids = {}
 
193
        # Mapping trans_id -> path in old tree
132
194
        self._tree_id_paths = {}
133
195
        # Cache of realpath results, to speed up canonical_path
134
196
        self._realpaths = {}
135
197
        # Cache of relpath results, to speed up canonical_path
136
198
        self._relpaths = {}
 
199
        # The trans_id that will be used as the tree root
137
200
        self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
 
201
        # Indictor of whether the transform has been applied
138
202
        self.__done = False
 
203
        # A progress bar
139
204
        self._pb = pb
 
205
        # A counter of how many files have been renamed
140
206
        self.rename_count = 0
141
207
 
142
208
    def __get_root(self):
166
232
            except OSError:
167
233
                # We don't especially care *why* the dir is immortal.
168
234
                raise ImmortalLimbo(self._limbodir)
 
235
            try:
 
236
                os.rmdir(self._deletiondir)
 
237
            except OSError:
 
238
                raise errors.ImmortalPendingDeletion(self._deletiondir)
169
239
        finally:
170
240
            self._tree.unlock()
171
241
            self._tree = None
371
441
        target is a bytestring.
372
442
        See also new_symlink.
373
443
        """
374
 
        os.symlink(target, self._limbo_name(trans_id))
375
 
        unique_add(self._new_contents, trans_id, 'symlink')
 
444
        if has_symlinks():
 
445
            os.symlink(target, self._limbo_name(trans_id))
 
446
            unique_add(self._new_contents, trans_id, 'symlink')
 
447
        else:
 
448
            try:
 
449
                path = FinalPaths(self).get_path(trans_id)
 
450
            except KeyError:
 
451
                path = None
 
452
            raise UnableCreateSymlink(path=path)
376
453
 
377
454
    def cancel_creation(self, trans_id):
378
455
        """Cancel the creation of new file contents."""
479
556
            return None
480
557
        # the file is old; the old id is still valid
481
558
        if self._new_root == trans_id:
482
 
            return self._tree.inventory.root.file_id
 
559
            return self._tree.get_root_id()
483
560
        return self._tree.inventory.path2id(path)
484
561
 
485
562
    def final_file_id(self, trans_id):
581
658
                        self.tree_kind(t) == 'directory'])
582
659
        for trans_id in self._removed_id:
583
660
            file_id = self.tree_file_id(trans_id)
584
 
            if self._tree.inventory[file_id].kind == 'directory':
 
661
            if file_id is not None:
 
662
                if self._tree.inventory[file_id].kind == 'directory':
 
663
                    parents.append(trans_id)
 
664
            elif self.tree_kind(trans_id) == 'directory':
585
665
                parents.append(trans_id)
586
666
 
587
667
        for parent_id in parents:
597
677
        try:
598
678
            children = os.listdir(self._tree.abspath(path))
599
679
        except OSError, e:
600
 
            if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
 
680
            if e.errno not in (errno.ENOENT, errno.ESRCH, errno.ENOTDIR):
601
681
                raise
602
682
            return
603
683
            
717
797
    def _duplicate_entries(self, by_parent):
718
798
        """No directory may have two entries with the same name."""
719
799
        conflicts = []
 
800
        if (self._new_name, self._new_parent) == ({}, {}):
 
801
            return conflicts
720
802
        for children in by_parent.itervalues():
721
803
            name_ids = [(self.final_name(t), t) for t in children]
 
804
            if not self._tree.case_sensitive:
 
805
                name_ids = [(n.lower(), t) for n, t in name_ids]
722
806
            name_ids.sort()
723
807
            last_name = None
724
808
            last_trans_id = None
783
867
            return True
784
868
        return False
785
869
            
786
 
    def apply(self):
 
870
    def apply(self, no_conflicts=False, _mover=None):
787
871
        """Apply all changes to the inventory and filesystem.
788
872
        
789
873
        If filesystem or inventory conflicts are present, MalformedTransform
790
874
        will be thrown.
791
875
 
792
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
793
881
        """
794
 
        conflicts = self.find_conflicts()
795
 
        if len(conflicts) != 0:
796
 
            raise MalformedTransform(conflicts=conflicts)
 
882
        if not no_conflicts:
 
883
            conflicts = self.find_conflicts()
 
884
            if len(conflicts) != 0:
 
885
                raise MalformedTransform(conflicts=conflicts)
797
886
        inv = self._tree.inventory
798
887
        inventory_delta = []
799
888
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
800
889
        try:
801
 
            child_pb.update('Apply phase', 0, 2)
802
 
            self._apply_removals(inv, inventory_delta)
803
 
            child_pb.update('Apply phase', 1, 2)
804
 
            modified_paths = self._apply_insertions(inv, inventory_delta)
 
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()
805
905
        finally:
806
906
            child_pb.finished()
807
907
        self._tree.apply_inventory_delta(inventory_delta)
829
929
                # the direct path can only be used if no other file has
830
930
                # already taken this pathname, i.e. if the name is unused, or
831
931
                # if it is already associated with this trans_id.
832
 
                elif (self._limbo_children_names[parent].get(filename)
833
 
                      in (trans_id, None)):
834
 
                    use_direct_path = True
 
932
                elif self._tree.case_sensitive:
 
933
                    if (self._limbo_children_names[parent].get(filename)
 
934
                        in (trans_id, None)):
 
935
                        use_direct_path = True
 
936
                else:
 
937
                    for l_filename, l_trans_id in\
 
938
                        self._limbo_children_names[parent].iteritems():
 
939
                        if l_trans_id == trans_id:
 
940
                            continue
 
941
                        if l_filename.lower() == filename.lower():
 
942
                            break
 
943
                    else:
 
944
                        use_direct_path = True
 
945
 
835
946
        if use_direct_path:
836
947
            limbo_name = pathjoin(self._limbo_files[parent], filename)
837
948
            self._limbo_children[parent].add(trans_id)
842
953
        self._limbo_files[trans_id] = limbo_name
843
954
        return limbo_name
844
955
 
845
 
    def _apply_removals(self, inv, inventory_delta):
 
956
    def _apply_removals(self, inv, inventory_delta, mover):
846
957
        """Perform tree operations that remove directory/inventory names.
847
958
        
848
959
        That is, delete files that are to be deleted, and put any files that
858
969
                child_pb.update('removing file', num, len(tree_paths))
859
970
                full_path = self._tree.abspath(path)
860
971
                if trans_id in self._removed_contents:
861
 
                    delete_any(full_path)
 
972
                    mover.pre_delete(full_path, os.path.join(self._deletiondir,
 
973
                                     trans_id))
862
974
                elif trans_id in self._new_name or trans_id in \
863
975
                    self._new_parent:
864
976
                    try:
865
 
                        os.rename(full_path, self._limbo_name(trans_id))
 
977
                        mover.rename(full_path, self._limbo_name(trans_id))
866
978
                    except OSError, e:
867
979
                        if e.errno != errno.ENOENT:
868
980
                            raise
870
982
                        self.rename_count += 1
871
983
                if trans_id in self._removed_id:
872
984
                    if trans_id == self._new_root:
873
 
                        file_id = self._tree.inventory.root.file_id
 
985
                        file_id = self._tree.get_root_id()
874
986
                    else:
875
987
                        file_id = self.tree_file_id(trans_id)
876
 
                    assert file_id is not None
877
 
                    inventory_delta.append((path, None, file_id, None))
 
988
                    if file_id is not None:
 
989
                        inventory_delta.append((path, None, file_id, None))
878
990
        finally:
879
991
            child_pb.finished()
880
992
 
881
 
    def _apply_insertions(self, inv, inventory_delta):
 
993
    def _apply_insertions(self, inv, inventory_delta, mover):
882
994
        """Perform tree operations that insert directory/inventory names.
883
995
        
884
996
        That is, create any files that need to be created, and restore from
888
1000
        new_paths = self.new_paths()
889
1001
        modified_paths = []
890
1002
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1003
        completed_new = []
891
1004
        try:
892
1005
            for num, (path, trans_id) in enumerate(new_paths):
893
1006
                new_entry = None
901
1014
                    full_path = self._tree.abspath(path)
902
1015
                    if trans_id in self._needs_rename:
903
1016
                        try:
904
 
                            os.rename(self._limbo_name(trans_id), full_path)
 
1017
                            mover.rename(self._limbo_name(trans_id), full_path)
905
1018
                        except OSError, e:
906
1019
                            # We may be renaming a dangling inventory id
907
1020
                            if e.errno != errno.ENOENT:
910
1023
                            self.rename_count += 1
911
1024
                    if trans_id in self._new_contents:
912
1025
                        modified_paths.append(full_path)
913
 
                        del self._new_contents[trans_id]
 
1026
                        completed_new.append(trans_id)
914
1027
 
915
1028
                if trans_id in self._new_id:
916
1029
                    if kind is None:
955
1068
                                            new_entry))
956
1069
        finally:
957
1070
            child_pb.finished()
 
1071
        for trans_id in completed_new:
 
1072
            del self._new_contents[trans_id]
958
1073
        return modified_paths
959
1074
 
960
1075
    def _set_executability(self, path, entry, trans_id):
1202
1317
            self._known_paths[trans_id] = self._determine_path(trans_id)
1203
1318
        return self._known_paths[trans_id]
1204
1319
 
 
1320
 
1205
1321
def topology_sorted_ids(tree):
1206
1322
    """Determine the topological order of the ids in a tree"""
1207
1323
    file_ids = list(tree)
1209
1325
    return file_ids
1210
1326
 
1211
1327
 
1212
 
def build_tree(tree, wt):
 
1328
def build_tree(tree, wt, accelerator_tree=None):
1213
1329
    """Create working tree for a branch, using a TreeTransform.
1214
1330
    
1215
1331
    This function should be used on empty trees, having a tree root at most.
1222
1338
    - Otherwise, if the content on disk matches the content we are building,
1223
1339
      it is silently replaced.
1224
1340
    - Otherwise, conflict resolution will move the old file to 'oldname.moved'.
 
1341
 
 
1342
    :param tree: The tree to convert wt into a copy of
 
1343
    :param wt: The working tree that files will be placed into
 
1344
    :param accelerator_tree: A tree which can be used for retrieving file
 
1345
        contents more quickly than tree itself, i.e. a workingtree.  tree
 
1346
        will be used for cases where accelerator_tree's content is different.
1225
1347
    """
1226
1348
    wt.lock_tree_write()
1227
1349
    try:
1228
1350
        tree.lock_read()
1229
1351
        try:
1230
 
            return _build_tree(tree, wt)
 
1352
            if accelerator_tree is not None:
 
1353
                accelerator_tree.lock_read()
 
1354
            try:
 
1355
                return _build_tree(tree, wt, accelerator_tree)
 
1356
            finally:
 
1357
                if accelerator_tree is not None:
 
1358
                    accelerator_tree.unlock()
1231
1359
        finally:
1232
1360
            tree.unlock()
1233
1361
    finally:
1234
1362
        wt.unlock()
1235
1363
 
1236
 
def _build_tree(tree, wt):
 
1364
 
 
1365
def _build_tree(tree, wt, accelerator_tree):
1237
1366
    """See build_tree."""
1238
1367
    if len(wt.inventory) > 1:  # more than just a root
1239
1368
        raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1248
1377
        # is set within the tree, nor setting the root and thus
1249
1378
        # marking the tree as dirty, because we use two different
1250
1379
        # idioms here: tree interfaces and inventory interfaces.
1251
 
        if wt.path2id('') != tree.inventory.root.file_id:
1252
 
            wt.set_root_id(tree.inventory.root.file_id)
 
1380
        if wt.get_root_id() != tree.get_root_id():
 
1381
            wt.set_root_id(tree.get_root_id())
1253
1382
            wt.flush()
1254
1383
    tt = TreeTransform(wt)
1255
1384
    divert = set()
1259
1388
            tt.trans_id_tree_file_id(wt.get_root_id())
1260
1389
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
1261
1390
        try:
 
1391
            deferred_contents = []
1262
1392
            for num, (tree_path, entry) in \
1263
1393
                enumerate(tree.inventory.iter_entries_by_dir()):
1264
 
                pb.update("Building tree", num, len(tree.inventory))
 
1394
                pb.update("Building tree", num - len(deferred_contents),
 
1395
                          len(tree.inventory))
1265
1396
                if entry.parent_id is None:
1266
1397
                    continue
1267
1398
                reparent = False
1290
1421
                        'entry %s parent id %r is not in file_trans_id %r'
1291
1422
                        % (entry, entry.parent_id, file_trans_id))
1292
1423
                parent_id = file_trans_id[entry.parent_id]
1293
 
                file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1294
 
                                                      tree)
 
1424
                if entry.kind == 'file':
 
1425
                    # We *almost* replicate new_by_entry, so that we can defer
 
1426
                    # getting the file text, and get them all at once.
 
1427
                    trans_id = tt.create_path(entry.name, parent_id)
 
1428
                    file_trans_id[file_id] = trans_id
 
1429
                    tt.version_file(entry.file_id, trans_id)
 
1430
                    executable = tree.is_executable(entry.file_id, tree_path)
 
1431
                    if executable is not None:
 
1432
                        tt.set_executability(executable, trans_id)
 
1433
                    deferred_contents.append((entry.file_id, trans_id))
 
1434
                else:
 
1435
                    file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
 
1436
                                                          tree)
1295
1437
                if reparent:
1296
1438
                    new_trans_id = file_trans_id[file_id]
1297
1439
                    old_parent = tt.trans_id_tree_path(tree_path)
1298
1440
                    _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))
1299
1448
        finally:
1300
1449
            pb.finished()
1301
1450
        pp.next_phase()
1309
1458
            wt.add_conflicts(conflicts)
1310
1459
        except errors.UnsupportedOperation:
1311
1460
            pass
1312
 
        result = tt.apply()
 
1461
        result = tt.apply(no_conflicts=True)
1313
1462
    finally:
1314
1463
        tt.finalize()
1315
1464
        top_pb.finished()
1316
1465
    return result
1317
1466
 
1318
1467
 
 
1468
def _iter_files_bytes_accelerated(tree, accelerator_tree, desired_files):
 
1469
    if accelerator_tree is None:
 
1470
        new_desired_files = desired_files
 
1471
    else:
 
1472
        iter = accelerator_tree._iter_changes(tree, include_unchanged=True)
 
1473
        unchanged = dict((f, p[1]) for (f, p, c, v, d, n, k, e)
 
1474
                         in iter if not c)
 
1475
        new_desired_files = []
 
1476
        for file_id, identifier in desired_files:
 
1477
            accelerator_path = unchanged.get(file_id)
 
1478
            if accelerator_path is None:
 
1479
                new_desired_files.append((file_id, identifier))
 
1480
                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
 
1490
 
 
1491
 
1319
1492
def _reparent_children(tt, old_parent, new_parent):
1320
1493
    for child in tt.iter_tree_children(old_parent):
1321
1494
        tt.adjust_path(tt.final_name(child), new_parent, child)
1322
1495
 
 
1496
def _reparent_transform_children(tt, old_parent, new_parent):
 
1497
    by_parent = tt.by_parent()
 
1498
    for child in by_parent[old_parent]:
 
1499
        tt.adjust_path(tt.final_name(child), new_parent, child)
1323
1500
 
1324
1501
def _content_match(tree, entry, file_id, kind, target_path):
1325
1502
    if entry.kind != kind:
1384
1561
    else:
1385
1562
        raise errors.BadFileKindError(name, kind)
1386
1563
 
 
1564
 
1387
1565
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1388
1566
    """Create new file contents according to an inventory entry."""
1389
1567
    if entry.kind == "file":
1395
1573
    elif entry.kind == "directory":
1396
1574
        tt.create_directory(trans_id)
1397
1575
 
 
1576
 
1398
1577
def create_entry_executability(tt, entry, trans_id):
1399
1578
    """Set the executability of a trans_id according to an inventory entry"""
1400
1579
    if entry.kind == "file":
1418
1597
        working_tree.unlock()
1419
1598
 
1420
1599
 
 
1600
@deprecated_function(zero_ninety)
1421
1601
def change_entry(tt, file_id, working_tree, target_tree, 
1422
1602
                 trans_id_file_id, backups, trans_id, by_parent):
1423
1603
    """Replace a file_id's contents with those from a target tree."""
 
1604
    if file_id is None and target_tree is None:
 
1605
        # skip the logic altogether in the deprecation test
 
1606
        return
1424
1607
    e_trans_id = trans_id_file_id(file_id)
1425
1608
    entry = target_tree.inventory[file_id]
1426
1609
    has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry, 
1518
1701
        pp.next_phase()
1519
1702
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1520
1703
        try:
1521
 
            raw_conflicts = resolve_conflicts(tt, child_pb)
 
1704
            raw_conflicts = resolve_conflicts(tt, child_pb,
 
1705
                lambda t, c: conflict_pass(t, c, target_tree))
1522
1706
        finally:
1523
1707
            child_pb.finished()
1524
1708
        conflicts = cook_conflicts(raw_conflicts, tt)
1549
1733
        skip_root = False
1550
1734
    basis_tree = None
1551
1735
    try:
 
1736
        deferred_files = []
1552
1737
        for id_num, (file_id, path, changed_content, versioned, parent, name,
1553
1738
                kind, executable) in enumerate(change_list):
1554
1739
            if skip_root and file_id[0] is not None and parent[0] is None:
1594
1779
                    tt.create_symlink(target_tree.get_symlink_target(file_id),
1595
1780
                                      trans_id)
1596
1781
                elif kind[1] == 'file':
1597
 
                    tt.create_file(target_tree.get_file_lines(file_id),
1598
 
                                   trans_id, mode_id)
 
1782
                    deferred_files.append((file_id, (trans_id, mode_id)))
1599
1783
                    if basis_tree is None:
1600
1784
                        basis_tree = working_tree.basis_tree()
1601
1785
                        basis_tree.lock_read()
1622
1806
                    name[1], tt.trans_id_file_id(parent[1]), trans_id)
1623
1807
            if executable[0] != executable[1] and kind[1] == "file":
1624
1808
                tt.set_executability(executable[1], trans_id)
 
1809
        for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
 
1810
            deferred_files):
 
1811
            tt.create_file(bytes, trans_id, mode_id)
1625
1812
    finally:
1626
1813
        if basis_tree is not None:
1627
1814
            basis_tree.unlock()
1645
1832
        pb.clear()
1646
1833
 
1647
1834
 
1648
 
def conflict_pass(tt, conflicts):
1649
 
    """Resolve some classes of conflicts."""
 
1835
def conflict_pass(tt, conflicts, path_tree=None):
 
1836
    """Resolve some classes of conflicts.
 
1837
 
 
1838
    :param tt: The transform to resolve conflicts in
 
1839
    :param conflicts: The conflicts to resolve
 
1840
    :param path_tree: A Tree to get supplemental paths from
 
1841
    """
1650
1842
    new_conflicts = set()
1651
1843
    for c_type, conflict in ((c[0], c) for c in conflicts):
1652
1844
        if c_type == 'duplicate id':
1655
1847
                               conflict[1], conflict[2], ))
1656
1848
        elif c_type == 'duplicate':
1657
1849
            # files that were renamed take precedence
1658
 
            new_name = tt.final_name(conflict[1])+'.moved'
1659
1850
            final_parent = tt.final_parent(conflict[1])
1660
1851
            if tt.path_changed(conflict[1]):
1661
 
                tt.adjust_path(new_name, final_parent, conflict[2])
1662
 
                new_conflicts.add((c_type, 'Moved existing file to', 
1663
 
                                   conflict[2], conflict[1]))
 
1852
                existing_file, new_file = conflict[2], conflict[1]
1664
1853
            else:
1665
 
                tt.adjust_path(new_name, final_parent, conflict[1])
1666
 
                new_conflicts.add((c_type, 'Moved existing file to', 
1667
 
                                  conflict[1], conflict[2]))
 
1854
                existing_file, new_file = conflict[1], conflict[2]
 
1855
            new_name = tt.final_name(existing_file)+'.moved'
 
1856
            tt.adjust_path(new_name, final_parent, existing_file)
 
1857
            new_conflicts.add((c_type, 'Moved existing file to', 
 
1858
                               existing_file, new_file))
1668
1859
        elif c_type == 'parent loop':
1669
1860
            # break the loop by undoing one of the ops that caused the loop
1670
1861
            cur = conflict[1]
1683
1874
            except KeyError:
1684
1875
                tt.create_directory(trans_id)
1685
1876
                new_conflicts.add((c_type, 'Created directory', trans_id))
 
1877
                try:
 
1878
                    tt.final_name(trans_id)
 
1879
                except NoFinalPath:
 
1880
                    if path_tree is not None:
 
1881
                        file_id = tt.final_file_id(trans_id)
 
1882
                        entry = path_tree.inventory[file_id]
 
1883
                        parent_trans_id = tt.trans_id_file_id(entry.parent_id)
 
1884
                        tt.adjust_path(entry.name, parent_trans_id, trans_id)
1686
1885
        elif c_type == 'unversioned parent':
1687
1886
            tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1688
1887
            new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
 
1888
        elif c_type == 'non-directory parent':
 
1889
            parent_id = conflict[1]
 
1890
            parent_parent = tt.final_parent(parent_id)
 
1891
            parent_name = tt.final_name(parent_id)
 
1892
            parent_file_id = tt.final_file_id(parent_id)
 
1893
            new_parent_id = tt.new_directory(parent_name + '.new',
 
1894
                parent_parent, parent_file_id)
 
1895
            _reparent_transform_children(tt, parent_id, new_parent_id)
 
1896
            tt.unversion_file(parent_id)
 
1897
            new_conflicts.add((c_type, 'Created directory', new_parent_id))
1689
1898
    return new_conflicts
1690
1899
 
1691
1900
 
1715
1924
                                   file_id=modified_id, 
1716
1925
                                   conflict_path=conflicting_path,
1717
1926
                                   conflict_file_id=conflicting_id)
 
1927
 
 
1928
 
 
1929
class _FileMover(object):
 
1930
    """Moves and deletes files for TreeTransform, tracking operations"""
 
1931
 
 
1932
    def __init__(self):
 
1933
        self.past_renames = []
 
1934
        self.pending_deletions = []
 
1935
 
 
1936
    def rename(self, from_, to):
 
1937
        """Rename a file from one path to another.  Functions like os.rename"""
 
1938
        try:
 
1939
            os.rename(from_, to)
 
1940
        except OSError, e:
 
1941
            if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
 
1942
                raise errors.FileExists(to, str(e))
 
1943
            raise
 
1944
        self.past_renames.append((from_, to))
 
1945
 
 
1946
    def pre_delete(self, from_, to):
 
1947
        """Rename a file out of the way and mark it for deletion.
 
1948
 
 
1949
        Unlike os.unlink, this works equally well for files and directories.
 
1950
        :param from_: The current file path
 
1951
        :param to: A temporary path for the file
 
1952
        """
 
1953
        self.rename(from_, to)
 
1954
        self.pending_deletions.append(to)
 
1955
 
 
1956
    def rollback(self):
 
1957
        """Reverse all renames that have been performed"""
 
1958
        for from_, to in reversed(self.past_renames):
 
1959
            os.rename(to, from_)
 
1960
        # after rollback, don't reuse _FileMover
 
1961
        past_renames = None
 
1962
        pending_deletions = None
 
1963
 
 
1964
    def apply_deletions(self):
 
1965
        """Apply all marked deletions"""
 
1966
        for path in self.pending_deletions:
 
1967
            delete_any(path)
 
1968
        # after apply_deletions, don't reuse _FileMover
 
1969
        past_renames = None
 
1970
        pending_deletions = None