~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transform.py

Added more docs

Show diffs side-by-side

added added

removed removed

Lines of Context:
18
18
import errno
19
19
from stat import S_ISREG
20
20
 
 
21
from bzrlib import BZRDIR
21
22
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
22
 
                           ReusingTransform, NotVersionedError, CantMoveRoot,
23
 
                           ExistingLimbo, ImmortalLimbo)
 
23
                           ReusingTransform, NotVersionedError, CantMoveRoot)
24
24
from bzrlib.inventory import InventoryEntry
25
 
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
26
 
                            delete_any)
27
 
from bzrlib.progress import DummyProgress, ProgressPhase
28
 
from bzrlib.trace import mutter, warning
29
 
import bzrlib.ui 
30
 
 
 
25
from bzrlib.osutils import file_kind, supports_executable, pathjoin
 
26
from bzrlib.trace import mutter
31
27
 
32
28
ROOT_PARENT = "root-parent"
33
29
 
34
 
 
35
30
def unique_add(map, key, value):
36
31
    if key in map:
37
32
        raise DuplicateKey(key=key)
38
33
    map[key] = value
39
34
 
40
 
 
41
 
class _TransformResults(object):
42
 
    def __init__(self, modified_paths):
43
 
        object.__init__(self)
44
 
        self.modified_paths = modified_paths
45
 
 
46
 
 
47
35
class TreeTransform(object):
48
 
    """Represent a tree transformation.
49
 
    
50
 
    This object is designed to support incremental generation of the transform,
51
 
    in any order.  
52
 
    
53
 
    It is easy to produce malformed transforms, but they are generally
54
 
    harmless.  Attempting to apply a malformed transform will cause an
55
 
    exception to be raised before any modifications are made to the tree.  
56
 
 
57
 
    Many kinds of malformed transforms can be corrected with the 
58
 
    resolve_conflicts function.  The remaining ones indicate programming error,
59
 
    such as trying to create a file with no path.
60
 
 
61
 
    Two sets of file creation methods are supplied.  Convenience methods are:
62
 
     * new_file
63
 
     * new_directory
64
 
     * new_symlink
65
 
 
66
 
    These are composed of the low-level methods:
67
 
     * create_path
68
 
     * create_file or create_directory or create_symlink
69
 
     * version_file
70
 
     * set_executability
71
 
    """
72
 
    def __init__(self, tree, pb=DummyProgress()):
 
36
    """Represent a tree transformation."""
 
37
    def __init__(self, tree):
73
38
        """Note: a write lock is taken on the tree.
74
39
        
75
40
        Use TreeTransform.finalize() to release the lock
77
42
        object.__init__(self)
78
43
        self._tree = tree
79
44
        self._tree.lock_write()
80
 
        try:
81
 
            control_files = self._tree._control_files
82
 
            self._limbodir = control_files.controlfilename('limbo')
83
 
            try:
84
 
                os.mkdir(self._limbodir)
85
 
            except OSError, e:
86
 
                if e.errno == errno.EEXIST:
87
 
                    raise ExistingLimbo(self._limbodir)
88
 
        except: 
89
 
            self._tree.unlock()
90
 
            raise
91
 
 
92
45
        self._id_number = 0
93
46
        self._new_name = {}
94
47
        self._new_parent = {}
101
54
        self._removed_id = set()
102
55
        self._tree_path_ids = {}
103
56
        self._tree_id_paths = {}
104
 
        self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
 
57
        self._new_root = self.get_id_tree(tree.get_root_id())
105
58
        self.__done = False
106
 
        self._pb = pb
 
59
        # XXX use the WorkingTree LockableFiles, when available
 
60
        control_files = self._tree.branch.control_files
 
61
        self._limbodir = control_files.controlfilename('limbo')
 
62
        os.mkdir(self._limbodir)
107
63
 
108
64
    def __get_root(self):
109
65
        return self._new_root
114
70
        """Release the working tree lock, if held, clean up limbo dir."""
115
71
        if self._tree is None:
116
72
            return
117
 
        try:
118
 
            for trans_id, kind in self._new_contents.iteritems():
119
 
                path = self._limbo_name(trans_id)
120
 
                if kind == "directory":
121
 
                    os.rmdir(path)
122
 
                else:
123
 
                    os.unlink(path)
124
 
            try:
125
 
                os.rmdir(self._limbodir)
126
 
            except OSError:
127
 
                # We don't especially care *why* the dir is immortal.
128
 
                raise ImmortalLimbo(self._limbodir)
129
 
        finally:
130
 
            self._tree.unlock()
131
 
            self._tree = None
 
73
        for trans_id, kind in self._new_contents.iteritems():
 
74
            path = self._limbo_name(trans_id)
 
75
            if kind == "directory":
 
76
                os.rmdir(path)
 
77
            else:
 
78
                os.unlink(path)
 
79
        os.rmdir(self._limbodir)
 
80
        self._tree.unlock()
 
81
        self._tree = None
132
82
 
133
83
    def _assign_id(self):
134
84
        """Produce a new tranform id"""
174
124
        # the physical root needs a new transaction id
175
125
        self._tree_path_ids.pop("")
176
126
        self._tree_id_paths.pop(old_root)
177
 
        self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
 
127
        self._new_root = self.get_id_tree(self._tree.get_root_id())
178
128
        if parent == old_root:
179
129
            parent = self._new_root
180
130
        self.adjust_path(name, parent, old_root)
182
132
        self.version_file(old_root_file_id, old_root)
183
133
        self.unversion_file(self._new_root)
184
134
 
185
 
    def trans_id_tree_file_id(self, inventory_id):
 
135
    def get_id_tree(self, inventory_id):
186
136
        """Determine the transaction id of a working tree file.
187
137
        
188
138
        This reflects only files that already exist, not ones that will be
189
139
        added by transactions.
190
140
        """
191
141
        path = self._tree.inventory.id2path(inventory_id)
192
 
        return self.trans_id_tree_path(path)
 
142
        return self.get_tree_path_id(path)
193
143
 
194
 
    def trans_id_file_id(self, file_id):
 
144
    def get_trans_id(self, file_id):
195
145
        """Determine or set the transaction id associated with a file ID.
196
146
        A new id is only created for file_ids that were never present.  If
197
147
        a transaction has been unversioned, it is deliberately still returned.
200
150
        if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
201
151
            return self._r_new_id[file_id]
202
152
        elif file_id in self._tree.inventory:
203
 
            return self.trans_id_tree_file_id(file_id)
 
153
            return self.get_id_tree(file_id)
204
154
        elif file_id in self._non_present_ids:
205
155
            return self._non_present_ids[file_id]
206
156
        else:
213
163
        # don't follow final symlinks
214
164
        dirname, basename = os.path.split(self._tree.abspath(path))
215
165
        dirname = os.path.realpath(dirname)
216
 
        return self._tree.relpath(pathjoin(dirname, basename))
 
166
        return self._tree.relpath(os.path.join(dirname, basename))
217
167
 
218
 
    def trans_id_tree_path(self, path):
 
168
    def get_tree_path_id(self, path):
219
169
        """Determine (and maybe set) the transaction ID for a tree path."""
220
170
        path = self.canonical_path(path)
221
171
        if path not in self._tree_path_ids:
228
178
        path = self._tree_id_paths[trans_id]
229
179
        if path == "":
230
180
            return ROOT_PARENT
231
 
        return self.trans_id_tree_path(os.path.dirname(path))
 
181
        return self.get_tree_path_id(os.path.dirname(path))
232
182
 
233
183
    def create_file(self, contents, trans_id, mode_id=None):
234
184
        """Schedule creation of a new file.
287
237
        os.symlink(target, self._limbo_name(trans_id))
288
238
        unique_add(self._new_contents, trans_id, 'symlink')
289
239
 
 
240
    @staticmethod
 
241
    def delete_any(full_path):
 
242
        """Delete a file or directory."""
 
243
        try:
 
244
            os.unlink(full_path)
 
245
        except OSError, e:
 
246
        # We may be renaming a dangling inventory id
 
247
            if e.errno != errno.EISDIR and e.errno != errno.EACCES:
 
248
                raise
 
249
            os.rmdir(full_path)
 
250
 
290
251
    def cancel_creation(self, trans_id):
291
252
        """Cancel the creation of new file contents."""
292
253
        del self._new_contents[trans_id]
293
 
        delete_any(self._limbo_name(trans_id))
 
254
        self.delete_any(self._limbo_name(trans_id))
294
255
 
295
256
    def delete_contents(self, trans_id):
296
257
        """Schedule the contents of a path entry for deletion"""
311
272
        self.unversion_file(trans_id)
312
273
 
313
274
    def set_executability(self, executability, trans_id):
314
 
        """Schedule setting of the 'execute' bit
315
 
        To unschedule, set to None
316
 
        """
 
275
        """Schedule setting of the 'execute' bit"""
317
276
        if executability is None:
318
277
            del self._new_executability[trans_id]
319
278
        else:
372
331
        else:
373
332
            return self.tree_kind(trans_id)
374
333
 
375
 
    def tree_file_id(self, trans_id):
 
334
    def get_tree_file_id(self, trans_id):
376
335
        """Determine the file id associated with the trans_id in the tree"""
377
336
        try:
378
337
            path = self._tree_id_paths[trans_id]
397
356
        except KeyError:
398
357
            if trans_id in self._removed_id:
399
358
                return None
400
 
        return self.tree_file_id(trans_id)
 
359
        return self.get_tree_file_id(trans_id)
 
360
 
401
361
 
402
362
    def inactive_file_id(self, trans_id):
403
363
        """Return the inactive file_id associated with a transaction id.
404
364
        That is, the one in the tree or in non_present_ids.
405
365
        The file_id may actually be active, too.
406
366
        """
407
 
        file_id = self.tree_file_id(trans_id)
 
367
        file_id = self.get_tree_file_id(trans_id)
408
368
        if file_id is not None:
409
369
            return file_id
410
370
        for key, value in self._non_present_ids.iteritems():
411
371
            if value == trans_id:
412
372
                return key
413
373
 
 
374
 
414
375
    def final_parent(self, trans_id):
415
376
        """Determine the parent file_id, after any changes are applied.
416
377
 
428
389
        except KeyError:
429
390
            return os.path.basename(self._tree_id_paths[trans_id])
430
391
 
431
 
    def by_parent(self):
 
392
    def _by_parent(self):
432
393
        """Return a map of parent: children for known parents.
433
394
        
434
395
        Only new paths and parents of tree files with assigned ids are used.
455
416
        # ensure all children of all existent parents are known
456
417
        # all children of non-existent parents are known, by definition.
457
418
        self._add_tree_children()
458
 
        by_parent = self.by_parent()
 
419
        by_parent = self._by_parent()
459
420
        conflicts.extend(self._unversioned_parents(by_parent))
460
421
        conflicts.extend(self._parent_loops())
461
422
        conflicts.extend(self._duplicate_entries(by_parent))
472
433
        Active parents are those which gain children, and those which are
473
434
        removed.  This is a necessary first step in detecting conflicts.
474
435
        """
475
 
        parents = self.by_parent().keys()
 
436
        parents = self._by_parent().keys()
476
437
        parents.extend([t for t in self._removed_contents if 
477
438
                        self.tree_kind(t) == 'directory'])
478
439
        for trans_id in self._removed_id:
479
 
            file_id = self.tree_file_id(trans_id)
 
440
            file_id = self.get_tree_file_id(trans_id)
480
441
            if self._tree.inventory[file_id].kind in ('directory', 
481
442
                                                      'root_directory'):
482
443
                parents.append(trans_id)
500
461
            
501
462
        for child in children:
502
463
            childpath = joinpath(path, child)
503
 
            if self._tree.is_control_filename(childpath):
 
464
            if childpath == BZRDIR:
504
465
                continue
505
 
            yield self.trans_id_tree_path(childpath)
506
 
 
507
 
    def has_named_child(self, by_parent, parent_id, name):
508
 
        try:
509
 
            children = by_parent[parent_id]
510
 
        except KeyError:
511
 
            children = []
512
 
        for child in children:
513
 
            if self.final_name(child) == name:
514
 
                return True
515
 
        try:
516
 
            path = self._tree_id_paths[parent_id]
517
 
        except KeyError:
518
 
            return False
519
 
        childpath = joinpath(path, name)
520
 
        child_id = self._tree_path_ids.get(childpath)
521
 
        if child_id is None:
522
 
            return lexists(self._tree.abspath(childpath))
523
 
        else:
524
 
            if tt.final_parent(child_id) != parent_id:
525
 
                return False
526
 
            if child_id in tt._removed_contents:
527
 
                # XXX What about dangling file-ids?
528
 
                return False
529
 
            else:
530
 
                return True
 
466
            yield self.get_tree_path_id(childpath)
531
467
 
532
468
    def _parent_loops(self):
533
469
        """No entry should be its own ancestor"""
620
556
                if name == last_name:
621
557
                    conflicts.append(('duplicate', last_trans_id, trans_id,
622
558
                    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
 
559
                last_name = name
 
560
                last_trans_id = trans_id
631
561
        return conflicts
632
562
 
633
563
    def _duplicate_ids(self):
634
564
        """Each inventory id may only be used once"""
635
565
        conflicts = []
636
 
        removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
 
566
        removed_tree_ids = set((self.get_tree_file_id(trans_id) for trans_id in
637
567
                                self._removed_id))
638
568
        active_tree_ids = set((f for f in self._tree.inventory if
639
569
                               f not in removed_tree_ids))
640
570
        for trans_id, file_id in self._new_id.iteritems():
641
571
            if file_id in active_tree_ids:
642
 
                old_trans_id = self.trans_id_tree_file_id(file_id)
 
572
                old_trans_id = self.get_id_tree(file_id)
643
573
                conflicts.append(('duplicate id', old_trans_id, trans_id))
644
574
        return conflicts
645
575
 
687
617
            raise MalformedTransform(conflicts=conflicts)
688
618
        limbo_inv = {}
689
619
        inv = self._tree.inventory
690
 
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
691
 
        try:
692
 
            child_pb.update('Apply phase', 0, 2)
693
 
            self._apply_removals(inv, limbo_inv)
694
 
            child_pb.update('Apply phase', 1, 2)
695
 
            modified_paths = self._apply_insertions(inv, limbo_inv)
696
 
        finally:
697
 
            child_pb.finished()
 
620
        self._apply_removals(inv, limbo_inv)
 
621
        self._apply_insertions(inv, limbo_inv)
698
622
        self._tree._write_inventory(inv)
699
623
        self.__done = True
700
624
        self.finalize()
701
 
        return _TransformResults(modified_paths)
702
625
 
703
626
    def _limbo_name(self, trans_id):
704
627
        """Generate the limbo name of a file"""
705
 
        return pathjoin(self._limbodir, trans_id)
 
628
        return os.path.join(self._limbodir, trans_id)
706
629
 
707
630
    def _apply_removals(self, inv, limbo_inv):
708
631
        """Perform tree operations that remove directory/inventory names.
713
636
        """
714
637
        tree_paths = list(self._tree_path_ids.iteritems())
715
638
        tree_paths.sort(reverse=True)
716
 
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
717
 
        try:
718
 
            for num, data in enumerate(tree_paths):
719
 
                path, trans_id = data
720
 
                child_pb.update('removing file', num, len(tree_paths))
721
 
                full_path = self._tree.abspath(path)
722
 
                if trans_id in self._removed_contents:
723
 
                    delete_any(full_path)
724
 
                elif trans_id in self._new_name or trans_id in \
725
 
                    self._new_parent:
726
 
                    try:
727
 
                        os.rename(full_path, self._limbo_name(trans_id))
728
 
                    except OSError, e:
729
 
                        if e.errno != errno.ENOENT:
730
 
                            raise
731
 
                if trans_id in self._removed_id:
732
 
                    if trans_id == self._new_root:
733
 
                        file_id = self._tree.inventory.root.file_id
734
 
                    else:
735
 
                        file_id = self.tree_file_id(trans_id)
 
639
        for path, trans_id in tree_paths:
 
640
            full_path = self._tree.abspath(path)
 
641
            if trans_id in self._removed_contents:
 
642
                self.delete_any(full_path)
 
643
            elif trans_id in self._new_name or trans_id in self._new_parent:
 
644
                try:
 
645
                    os.rename(full_path, self._limbo_name(trans_id))
 
646
                except OSError, e:
 
647
                    if e.errno != errno.ENOENT:
 
648
                        raise
 
649
            if trans_id in self._removed_id:
 
650
                if trans_id == self._new_root:
 
651
                    file_id = self._tree.inventory.root.file_id
 
652
                else:
 
653
                    file_id = self.get_tree_file_id(trans_id)
 
654
                del inv[file_id]
 
655
            elif trans_id in self._new_name or trans_id in self._new_parent:
 
656
                file_id = self.get_tree_file_id(trans_id)
 
657
                if file_id is not None:
 
658
                    limbo_inv[trans_id] = inv[file_id]
736
659
                    del inv[file_id]
737
 
                elif trans_id in self._new_name or trans_id in self._new_parent:
738
 
                    file_id = self.tree_file_id(trans_id)
739
 
                    if file_id is not None:
740
 
                        limbo_inv[trans_id] = inv[file_id]
741
 
                        del inv[file_id]
742
 
        finally:
743
 
            child_pb.finished()
744
660
 
745
661
    def _apply_insertions(self, inv, limbo_inv):
746
662
        """Perform tree operations that insert directory/inventory names.
749
665
        limbo any files that needed renaming.  This must be done in strict
750
666
        parent-to-child order.
751
667
        """
752
 
        new_paths = self.new_paths()
753
 
        modified_paths = []
754
 
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
755
 
        try:
756
 
            for num, (path, trans_id) in enumerate(new_paths):
757
 
                child_pb.update('adding file', num, len(new_paths))
 
668
        for path, trans_id in self.new_paths():
 
669
            try:
 
670
                kind = self._new_contents[trans_id]
 
671
            except KeyError:
 
672
                kind = contents = None
 
673
            if trans_id in self._new_contents or self.path_changed(trans_id):
 
674
                full_path = self._tree.abspath(path)
758
675
                try:
759
 
                    kind = self._new_contents[trans_id]
760
 
                except KeyError:
761
 
                    kind = contents = None
762
 
                if trans_id in self._new_contents or \
763
 
                    self.path_changed(trans_id):
764
 
                    full_path = self._tree.abspath(path)
765
 
                    try:
766
 
                        os.rename(self._limbo_name(trans_id), full_path)
767
 
                    except OSError, e:
768
 
                        # We may be renaming a dangling inventory id
769
 
                        if e.errno != errno.ENOENT:
770
 
                            raise
771
 
                    if trans_id in self._new_contents:
772
 
                        modified_paths.append(full_path)
773
 
                        del self._new_contents[trans_id]
774
 
 
775
 
                if trans_id in self._new_id:
776
 
                    if kind is None:
777
 
                        kind = file_kind(self._tree.abspath(path))
778
 
                    inv.add_path(path, kind, self._new_id[trans_id])
779
 
                elif trans_id in self._new_name or trans_id in\
780
 
                    self._new_parent:
781
 
                    entry = limbo_inv.get(trans_id)
782
 
                    if entry is not None:
783
 
                        entry.name = self.final_name(trans_id)
784
 
                        parent_path = os.path.dirname(path)
785
 
                        entry.parent_id = \
786
 
                            self._tree.inventory.path2id(parent_path)
787
 
                        inv.add(entry)
788
 
 
789
 
                # requires files and inventory entries to be in place
790
 
                if trans_id in self._new_executability:
791
 
                    self._set_executability(path, inv, trans_id)
792
 
        finally:
793
 
            child_pb.finished()
794
 
        return modified_paths
 
676
                    os.rename(self._limbo_name(trans_id), full_path)
 
677
                except OSError, e:
 
678
                    # We may be renaming a dangling inventory id
 
679
                    if e.errno != errno.ENOENT:
 
680
                        raise
 
681
                if trans_id in self._new_contents:
 
682
                    del self._new_contents[trans_id]
 
683
 
 
684
            if trans_id in self._new_id:
 
685
                if kind is None:
 
686
                    kind = file_kind(self._tree.abspath(path))
 
687
                inv.add_path(path, kind, self._new_id[trans_id])
 
688
            elif trans_id in self._new_name or trans_id in self._new_parent:
 
689
                entry = limbo_inv.get(trans_id)
 
690
                if entry is not None:
 
691
                    entry.name = self.final_name(trans_id)
 
692
                    parent_path = os.path.dirname(path)
 
693
                    entry.parent_id = self._tree.inventory.path2id(parent_path)
 
694
                    inv.add(entry)
 
695
 
 
696
            # requires files and inventory entries to be in place
 
697
            if trans_id in self._new_executability:
 
698
                self._set_executability(path, inv, trans_id)
795
699
 
796
700
    def _set_executability(self, path, inv, trans_id):
797
701
        """Set the executability of versioned files """
866
770
    if parent is None or parent == "":
867
771
        return child
868
772
    else:
869
 
        return pathjoin(parent, child)
870
 
 
 
773
        return os.path.join(parent, child)
871
774
 
872
775
class FinalPaths(object):
873
776
    """Make path calculation cheap by memoizing paths.
888
791
        if parent_id == self.transform.root:
889
792
            return name
890
793
        else:
891
 
            return pathjoin(self.get_path(parent_id), name)
 
794
            return os.path.join(self.get_path(parent_id), name)
892
795
 
893
796
    def get_path(self, trans_id):
894
797
        """Find the final path associated with a trans_id"""
902
805
    file_ids.sort(key=tree.id2path)
903
806
    return file_ids
904
807
 
905
 
def build_tree(tree, wt):
 
808
def build_tree(branch, tree):
906
809
    """Create working tree for a branch, using a Transaction."""
907
810
    file_trans_id = {}
908
 
    top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
909
 
    pp = ProgressPhase("Build phase", 2, top_pb)
 
811
    wt = branch.working_tree()
910
812
    tt = TreeTransform(wt)
911
813
    try:
912
 
        pp.next_phase()
913
 
        file_trans_id[wt.get_root_id()] = tt.trans_id_tree_file_id(wt.get_root_id())
 
814
        file_trans_id[wt.get_root_id()] = tt.get_id_tree(wt.get_root_id())
914
815
        file_ids = topology_sorted_ids(tree)
915
 
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
916
 
        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]
920
 
                if entry.parent_id is None:
921
 
                    continue
922
 
                if entry.parent_id not in file_trans_id:
923
 
                    raise repr(entry.parent_id)
924
 
                parent_id = file_trans_id[entry.parent_id]
925
 
                file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, 
926
 
                                                      tree)
927
 
        finally:
928
 
            pb.finished()
929
 
        pp.next_phase()
 
816
        for file_id in file_ids:
 
817
            entry = tree.inventory[file_id]
 
818
            if entry.parent_id is None:
 
819
                continue
 
820
            if entry.parent_id not in file_trans_id:
 
821
                raise repr(entry.parent_id)
 
822
            parent_id = file_trans_id[entry.parent_id]
 
823
            file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, tree)
930
824
        tt.apply()
931
825
    finally:
932
826
        tt.finalize()
933
 
        top_pb.finished()
934
827
 
935
828
def new_by_entry(tt, entry, parent_id, tree):
936
829
    """Create a new file according to its inventory entry"""
944
837
    elif kind == 'directory':
945
838
        return tt.new_directory(name, parent_id, entry.file_id)
946
839
    elif kind == 'symlink':
947
 
        target = tree.get_symlink_target(entry.file_id)
948
 
        return tt.new_symlink(name, parent_id, target, entry.file_id)
 
840
        target = entry.get_symlink_target(file_id)
 
841
        return tt.new_symlink(name, parent_id, target, file_id)
949
842
 
950
843
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
951
844
    """Create new file contents according to an inventory entry."""
971
864
    else:
972
865
        interesting_ids = set()
973
866
        for tree_path in filenames:
974
 
            not_found = True
975
867
            for tree in (working_tree, target_tree):
 
868
                not_found = True
976
869
                file_id = tree.inventory.path2id(tree_path)
977
870
                if file_id is not None:
978
871
                    interesting_ids.add(file_id)
979
872
                    not_found = False
980
 
            if not_found:
981
 
                raise NotVersionedError(path=tree_path)
 
873
                if not_found:
 
874
                    raise NotVersionedError(path=tree_path)
982
875
    return interesting_ids
983
876
 
984
877
 
985
878
def change_entry(tt, file_id, working_tree, target_tree, 
986
 
                 trans_id_file_id, backups, trans_id, by_parent):
 
879
                 get_trans_id, backups, trans_id):
987
880
    """Replace a file_id's contents with those from a target tree."""
988
 
    e_trans_id = trans_id_file_id(file_id)
 
881
    e_trans_id = get_trans_id(file_id)
989
882
    entry = target_tree.inventory[file_id]
990
883
    has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry, 
991
884
                                                           working_tree)
995
888
            if not backups:
996
889
                tt.delete_contents(e_trans_id)
997
890
            else:
998
 
                parent_trans_id = trans_id_file_id(entry.parent_id)
999
 
                backup_name = get_backup_name(entry, by_parent,
1000
 
                                              parent_trans_id, tt)
1001
 
                tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
 
891
                parent_trans_id = get_trans_id(entry.parent_id)
 
892
                tt.adjust_path(entry.name+"~", parent_trans_id, e_trans_id)
1002
893
                tt.unversion_file(e_trans_id)
1003
894
                e_trans_id = tt.create_path(entry.name, parent_trans_id)
1004
895
                tt.version_file(file_id, e_trans_id)
1018
909
        else:
1019
910
            adjust_path = False
1020
911
    if adjust_path:
1021
 
        parent_trans_id = trans_id_file_id(entry.parent_id)
 
912
        parent_trans_id = get_trans_id(entry.parent_id)
1022
913
        tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1023
914
 
1024
915
 
1025
 
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1026
 
    """Produce a backup-style name that appears to be available"""
1027
 
    def name_gen():
1028
 
        counter = 1
1029
 
        while True:
1030
 
            yield "%s.~%d~" % (entry.name, counter)
1031
 
            counter += 1
1032
 
    for name in name_gen():
1033
 
        if not tt.has_named_child(by_parent, parent_trans_id, name):
1034
 
            return name
1035
 
 
1036
916
def _entry_changes(file_id, entry, working_tree):
1037
917
    """Determine in which ways the inventory entry has changed.
1038
918
 
1061
941
            cur_entry._read_tree_state(working_tree.id2path(file_id), 
1062
942
                                       working_tree)
1063
943
            contents_mod, meta_mod = entry.detect_changes(cur_entry)
1064
 
            cur_entry._forget_tree_state()
1065
944
    return has_contents, contents_mod, meta_mod
1066
945
 
1067
946
 
1068
 
def revert(working_tree, target_tree, filenames, backups=False, 
1069
 
           pb=DummyProgress()):
 
947
def revert(working_tree, target_tree, filenames, backups=False):
1070
948
    """Revert a working tree's contents to those of a target tree."""
1071
949
    interesting_ids = find_interesting(working_tree, target_tree, filenames)
1072
950
    def interesting(file_id):
1073
951
        return interesting_ids is None or file_id in interesting_ids
1074
952
 
1075
 
    tt = TreeTransform(working_tree, pb)
 
953
    tt = TreeTransform(working_tree)
1076
954
    try:
1077
 
        merge_modified = working_tree.merge_modified()
1078
955
        trans_id = {}
1079
 
        def trans_id_file_id(file_id):
 
956
        def get_trans_id(file_id):
1080
957
            try:
1081
958
                return trans_id[file_id]
1082
959
            except KeyError:
1083
 
                return tt.trans_id_tree_file_id(file_id)
 
960
                return tt.get_id_tree(file_id)
1084
961
 
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]
1123
 
        finally:
1124
 
            child_pb.finished()
1125
 
        pp.next_phase()
1126
 
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1127
 
        try:
1128
 
            raw_conflicts = resolve_conflicts(tt, child_pb)
1129
 
        finally:
1130
 
            child_pb.finished()
1131
 
        conflicts = cook_conflicts(raw_conflicts, tt)
1132
 
        for conflict in conflicts:
1133
 
            warning(conflict)
1134
 
        pp.next_phase()
 
962
        for file_id in topology_sorted_ids(target_tree):
 
963
            if not interesting(file_id):
 
964
                continue
 
965
            if file_id not in working_tree.inventory:
 
966
                entry = target_tree.inventory[file_id]
 
967
                parent_id = get_trans_id(entry.parent_id)
 
968
                e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
 
969
                trans_id[file_id] = e_trans_id
 
970
            else:
 
971
                change_entry(tt, file_id, working_tree, target_tree, 
 
972
                             get_trans_id, backups, trans_id)
 
973
        for file_id in working_tree:
 
974
            if not interesting(file_id):
 
975
                continue
 
976
            if file_id not in target_tree:
 
977
                tt.unversion_file(tt.get_id_tree(file_id))
 
978
        resolve_conflicts(tt)
1135
979
        tt.apply()
1136
 
        working_tree.set_merge_modified({})
1137
980
    finally:
1138
981
        tt.finalize()
1139
 
        pb.clear()
1140
 
    return conflicts
1141
 
 
1142
 
 
1143
 
def resolve_conflicts(tt, pb=DummyProgress()):
 
982
 
 
983
 
 
984
def resolve_conflicts(tt):
1144
985
    """Make many conflict-resolution attempts, but die if they fail"""
1145
 
    new_conflicts = set()
1146
 
    try:
1147
 
        for n in range(10):
1148
 
            pb.update('Resolution pass', n+1, 10)
1149
 
            conflicts = tt.find_conflicts()
1150
 
            if len(conflicts) == 0:
1151
 
                return new_conflicts
1152
 
            new_conflicts.update(conflict_pass(tt, conflicts))
1153
 
        raise MalformedTransform(conflicts=conflicts)
1154
 
    finally:
1155
 
        pb.clear()
 
986
    for n in range(10):
 
987
        conflicts = tt.find_conflicts()
 
988
        if len(conflicts) == 0:
 
989
            return
 
990
        conflict_pass(tt, conflicts)
 
991
    raise MalformedTransform(conflicts=conflicts)
1156
992
 
1157
993
 
1158
994
def conflict_pass(tt, conflicts):
1159
995
    """Resolve some classes of conflicts."""
1160
 
    new_conflicts = set()
1161
996
    for c_type, conflict in ((c[0], c) for c in conflicts):
1162
997
        if c_type == 'duplicate id':
1163
998
            tt.unversion_file(conflict[1])
1164
 
            new_conflicts.add((c_type, 'Unversioned existing file',
1165
 
                               conflict[1], conflict[2], ))
1166
999
        elif c_type == 'duplicate':
1167
1000
            # files that were renamed take precedence
1168
1001
            new_name = tt.final_name(conflict[1])+'.moved'
1169
1002
            final_parent = tt.final_parent(conflict[1])
1170
1003
            if tt.path_changed(conflict[1]):
1171
1004
                tt.adjust_path(new_name, final_parent, conflict[2])
1172
 
                new_conflicts.add((c_type, 'Moved existing file to', 
1173
 
                                   conflict[2], conflict[1]))
1174
1005
            else:
1175
1006
                tt.adjust_path(new_name, final_parent, conflict[1])
1176
 
                new_conflicts.add((c_type, 'Moved existing file to', 
1177
 
                                  conflict[1], conflict[2]))
1178
1007
        elif c_type == 'parent loop':
1179
1008
            # break the loop by undoing one of the ops that caused the loop
1180
1009
            cur = conflict[1]
1181
1010
            while not tt.path_changed(cur):
1182
1011
                cur = tt.final_parent(cur)
1183
 
            new_conflicts.add((c_type, 'Cancelled move', cur,
1184
 
                               tt.final_parent(cur),))
1185
1012
            tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1186
 
            
1187
1013
        elif c_type == 'missing parent':
1188
1014
            trans_id = conflict[1]
1189
1015
            try:
1190
1016
                tt.cancel_deletion(trans_id)
1191
 
                new_conflicts.add((c_type, 'Not deleting', trans_id))
1192
1017
            except KeyError:
1193
1018
                tt.create_directory(trans_id)
1194
 
                new_conflicts.add((c_type, 'Created directory.', trans_id))
1195
1019
        elif c_type == 'unversioned parent':
1196
1020
            tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1197
 
            new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1198
 
    return new_conflicts
1199
 
 
1200
 
 
1201
 
def cook_conflicts(raw_conflicts, tt):
1202
 
    """Generate a list of cooked conflicts, sorted by file path"""
1203
 
    from bzrlib.conflicts import Conflict
1204
 
    conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1205
 
    return sorted(conflict_iter, key=Conflict.sort_key)
1206
 
 
1207
 
 
1208
 
def iter_cook_conflicts(raw_conflicts, tt):
1209
 
    from bzrlib.conflicts import Conflict
1210
 
    fp = FinalPaths(tt)
1211
 
    for conflict in raw_conflicts:
1212
 
        c_type = conflict[0]
1213
 
        action = conflict[1]
1214
 
        modified_path = fp.get_path(conflict[2])
1215
 
        modified_id = tt.final_file_id(conflict[2])
1216
 
        if len(conflict) == 3:
1217
 
            yield Conflict.factory(c_type, action=action, path=modified_path,
1218
 
                                     file_id=modified_id)
1219
 
             
1220
 
        else:
1221
 
            conflicting_path = fp.get_path(conflict[3])
1222
 
            conflicting_id = tt.final_file_id(conflict[3])
1223
 
            yield Conflict.factory(c_type, action=action, path=modified_path,
1224
 
                                   file_id=modified_id, 
1225
 
                                   conflict_path=conflicting_path,
1226
 
                                   conflict_file_id=conflicting_id)