~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/changeset.py

  • Committer: Robert Collins
  • Date: 2005-12-07 05:53:58 UTC
  • Revision ID: robertc@robertcollins.net-20051207055358-757787e694d89456
Test for the number of uses of self.working_tree() in branch.py

Show diffs side-by-side

added added

removed removed

Lines of Context:
25
25
import os.path
26
26
import errno
27
27
import stat
 
28
from tempfile import mkdtemp
28
29
from shutil import rmtree
29
30
from itertools import izip
30
31
 
31
32
from bzrlib.trace import mutter, warning
32
 
from bzrlib.osutils import rename, sha_file, pathjoin, mkdtemp
 
33
from bzrlib.osutils import rename, sha_file
33
34
import bzrlib
34
35
from bzrlib.errors import BzrCheckError
35
36
 
36
37
__docformat__ = "restructuredtext"
37
38
 
38
 
 
39
39
NULL_ID = "!NULL"
40
40
 
41
 
 
42
41
class OldFailedTreeOp(Exception):
43
42
    def __init__(self):
44
43
        Exception.__init__(self, "bzr-tree-change contains files from a"
45
44
                           " previous failed merge operation.")
46
 
 
47
 
 
48
45
def invert_dict(dict):
49
46
    newdict = {}
50
47
    for (key,value) in dict.iteritems():
59
56
        self.old_exec_flag = old_exec_flag
60
57
        self.new_exec_flag = new_exec_flag
61
58
 
62
 
    def apply(self, filename, conflict_handler):
63
 
        from_exec_flag = self.old_exec_flag
64
 
        to_exec_flag = self.new_exec_flag
 
59
    def apply(self, filename, conflict_handler, reverse=False):
 
60
        if not reverse:
 
61
            from_exec_flag = self.old_exec_flag
 
62
            to_exec_flag = self.new_exec_flag
 
63
        else:
 
64
            from_exec_flag = self.new_exec_flag
 
65
            to_exec_flag = self.old_exec_flag
65
66
        try:
66
67
            current_exec_flag = bool(os.stat(filename).st_mode & 0111)
67
68
        except OSError, e:
104
105
        return not (self == other)
105
106
 
106
107
 
107
 
def dir_create(filename, conflict_handler, reverse=False):
 
108
def dir_create(filename, conflict_handler, reverse):
108
109
    """Creates the directory, or deletes it if reverse is true.  Intended to be
109
110
    used with ReplaceContents.
110
111
 
149
150
    def __repr__(self):
150
151
        return "SymlinkCreate(%s)" % self.target
151
152
 
152
 
    def __call__(self, filename, conflict_handler, reverse=False):
 
153
    def __call__(self, filename, conflict_handler, reverse):
153
154
        """Creates or destroys the symlink.
154
155
 
155
156
        :param filename: The name of the symlink to create
178
179
    def __ne__(self, other):
179
180
        return not (self == other)
180
181
 
181
 
 
182
182
class FileCreate(object):
183
183
    """Create or delete a file (for use with ReplaceContents)"""
184
184
    def __init__(self, contents):
203
203
    def __ne__(self, other):
204
204
        return not (self == other)
205
205
 
206
 
    def __call__(self, filename, conflict_handler, reverse=False):
 
206
    def __call__(self, filename, conflict_handler, reverse):
207
207
        """Create or delete a file
208
208
 
209
209
        :param filename: The name of the file to create
235
235
                if conflict_handler.missing_for_rm(filename, undo) == "skip":
236
236
                    return
237
237
 
 
238
                    
238
239
 
239
240
class TreeFileCreate(object):
240
241
    """Create or delete a file (for use with ReplaceContents)"""
268
269
        in_file = file(filename, "rb")
269
270
        return sha_file(in_file) == self.tree.get_file_sha1(self.file_id)
270
271
 
271
 
    def __call__(self, filename, conflict_handler, reverse=False):
 
272
    def __call__(self, filename, conflict_handler, reverse):
272
273
        """Create or delete a file
273
274
 
274
275
        :param filename: The name of the file to create
300
301
                if conflict_handler.missing_for_rm(filename, undo) == "skip":
301
302
                    return
302
303
 
 
304
                    
 
305
 
 
306
def reversed(sequence):
 
307
    max = len(sequence) - 1
 
308
    for i in range(len(sequence)):
 
309
        yield sequence[max - i]
303
310
 
304
311
class ReplaceContents(object):
305
312
    """A contents-replacement framework.  It allows a file/directory/symlink to
337
344
    def __ne__(self, other):
338
345
        return not (self == other)
339
346
 
340
 
    def apply(self, filename, conflict_handler):
 
347
    def apply(self, filename, conflict_handler, reverse=False):
341
348
        """Applies the FileReplacement to the specified filename
342
349
 
343
350
        :param filename: The name of the file to apply changes to
344
351
        :type filename: str
 
352
        :param reverse: If true, apply the change in reverse
 
353
        :type reverse: bool
345
354
        """
346
 
        undo = self.old_contents
347
 
        perform = self.new_contents
 
355
        if not reverse:
 
356
            undo = self.old_contents
 
357
            perform = self.new_contents
 
358
        else:
 
359
            undo = self.new_contents
 
360
            perform = self.old_contents
348
361
        mode = None
349
362
        if undo is not None:
350
363
            try:
358
371
                    return
359
372
            undo(filename, conflict_handler, reverse=True)
360
373
        if perform is not None:
361
 
            perform(filename, conflict_handler)
 
374
            perform(filename, conflict_handler, reverse=False)
362
375
            if mode is not None:
363
376
                os.chmod(filename, mode)
364
377
 
368
381
    def is_deletion(self):
369
382
        return self.old_contents is not None and self.new_contents is None
370
383
 
 
384
class ApplySequence(object):
 
385
    def __init__(self, changes=None):
 
386
        self.changes = []
 
387
        if changes is not None:
 
388
            self.changes.extend(changes)
 
389
 
 
390
    def __eq__(self, other):
 
391
        if not isinstance(other, ApplySequence):
 
392
            return False
 
393
        elif len(other.changes) != len(self.changes):
 
394
            return False
 
395
        else:
 
396
            for i in range(len(self.changes)):
 
397
                if self.changes[i] != other.changes[i]:
 
398
                    return False
 
399
            return True
 
400
 
 
401
    def __ne__(self, other):
 
402
        return not (self == other)
 
403
 
 
404
    
 
405
    def apply(self, filename, conflict_handler, reverse=False):
 
406
        if not reverse:
 
407
            iter = self.changes
 
408
        else:
 
409
            iter = reversed(self.changes)
 
410
        for change in iter:
 
411
            change.apply(filename, conflict_handler, reverse)
 
412
 
371
413
 
372
414
class Diff3Merge(object):
373
415
    history_based = False
392
434
        return not (self == other)
393
435
 
394
436
    def dump_file(self, temp_dir, name, tree):
395
 
        out_path = pathjoin(temp_dir, name)
 
437
        out_path = os.path.join(temp_dir, name)
396
438
        out_file = file(out_path, "wb")
397
439
        in_file = tree.get_file(self.file_id)
398
440
        for line in in_file:
399
441
            out_file.write(line)
400
442
        return out_path
401
443
 
402
 
    def apply(self, filename, conflict_handler):
 
444
    def apply(self, filename, conflict_handler, reverse=False):
403
445
        import bzrlib.patch
404
 
        temp_dir = mkdtemp(prefix="bzr-", dir=os.path.dirname(filename))
 
446
        temp_dir = mkdtemp(prefix="bzr-")
405
447
        try:
406
 
            new_file = os.path.join(temp_dir, filename)
 
448
            new_file = filename+".new"
407
449
            base_file = self.dump_file(temp_dir, "base", self.base)
408
450
            other_file = self.dump_file(temp_dir, "other", self.other)
409
 
            base = base_file
410
 
            other = other_file
 
451
            if not reverse:
 
452
                base = base_file
 
453
                other = other_file
 
454
            else:
 
455
                base = other_file
 
456
                other = base_file
411
457
            status = bzrlib.patch.diff3(new_file, filename, base, other)
412
458
            if status == 0:
413
459
                os.chmod(new_file, os.stat(filename).st_mode)
436
482
    """
437
483
    return ReplaceContents(None, dir_create)
438
484
 
439
 
 
440
485
def DeleteDir():
441
486
    """Convenience function to delete a directory.
442
487
 
445
490
    """
446
491
    return ReplaceContents(dir_create, None)
447
492
 
448
 
 
449
493
def CreateFile(contents):
450
494
    """Convenience fucntion to create a file.
451
495
    
456
500
    """
457
501
    return ReplaceContents(None, FileCreate(contents))
458
502
 
459
 
 
460
503
def DeleteFile(contents):
461
504
    """Convenience fucntion to delete a file.
462
505
    
467
510
    """
468
511
    return ReplaceContents(FileCreate(contents), None)
469
512
 
470
 
 
471
513
def ReplaceFileContents(old_tree, new_tree, file_id):
472
514
    """Convenience fucntion to replace the contents of a file.
473
515
    
481
523
    return ReplaceContents(TreeFileCreate(old_tree, file_id), 
482
524
                           TreeFileCreate(new_tree, file_id))
483
525
 
484
 
 
485
526
def CreateSymlink(target):
486
527
    """Convenience fucntion to create a symlink.
487
528
    
492
533
    """
493
534
    return ReplaceContents(None, SymlinkCreate(target))
494
535
 
495
 
 
496
536
def DeleteSymlink(target):
497
537
    """Convenience fucntion to delete a symlink.
498
538
    
503
543
    """
504
544
    return ReplaceContents(SymlinkCreate(target), None)
505
545
 
506
 
 
507
546
def ChangeTarget(old_target, new_target):
508
547
    """Convenience fucntion to change the target of a symlink.
509
548
    
547
586
        msg = 'Child of !NULL is named "%s", not "./.".' % name
548
587
        InvalidEntry.__init__(self, entry, msg)
549
588
 
550
 
 
551
589
class NullIDAssigned(InvalidEntry):
552
590
    """The id !NULL was assigned to a real entry"""
553
591
    def __init__(self, entry):
559
597
        msg = '"!NULL" id assigned to a file "%s".' % entry.path
560
598
        InvalidEntry.__init__(self, entry, msg)
561
599
 
562
 
 
563
600
class ParentIDIsSelf(InvalidEntry):
564
601
    """An entry is marked as its own parent"""
565
602
    def __init__(self, entry):
572
609
            (entry.path, entry.id)
573
610
        InvalidEntry.__init__(self, entry, msg)
574
611
 
575
 
 
576
612
class ChangesetEntry(object):
577
613
    """An entry the changeset"""
578
614
    def __init__(self, id, parent, path):
608
644
        return os.path.dirname(self.path)
609
645
 
610
646
    def __set_dir(self, dir):
611
 
        self.path = pathjoin(dir, os.path.basename(self.path))
 
647
        self.path = os.path.join(dir, os.path.basename(self.path))
612
648
 
613
649
    dir = property(__get_dir, __set_dir)
614
650
    
618
654
        return os.path.basename(self.path)
619
655
 
620
656
    def __set_name(self, name):
621
 
        self.path = pathjoin(os.path.dirname(self.path), name)
 
657
        self.path = os.path.join(os.path.dirname(self.path), name)
622
658
 
623
659
    name = property(__get_name, __set_name)
624
660
 
628
664
        return os.path.dirname(self.new_path)
629
665
 
630
666
    def __set_new_dir(self, dir):
631
 
        self.new_path = pathjoin(dir, os.path.basename(self.new_path))
 
667
        self.new_path = os.path.join(dir, os.path.basename(self.new_path))
632
668
 
633
669
    new_dir = property(__get_new_dir, __set_new_dir)
634
670
 
638
674
        return os.path.basename(self.new_path)
639
675
 
640
676
    def __set_new_name(self, name):
641
 
        self.new_path = pathjoin(os.path.dirname(self.new_path), name)
 
677
        self.new_path = os.path.join(os.path.dirname(self.new_path), name)
642
678
 
643
679
    new_name = property(__get_new_name, __set_new_name)
644
680
 
647
683
 
648
684
        :rtype: bool
649
685
        """
 
686
 
650
687
        return (self.parent != self.new_parent or self.name != self.new_name)
651
688
 
652
 
    def is_deletion(self, reverse=False):
 
689
    def is_deletion(self, reverse):
653
690
        """Return true if applying the entry would delete a file/directory.
654
691
 
655
692
        :param reverse: if true, the changeset is being applied in reverse
657
694
        """
658
695
        return self.is_creation(not reverse)
659
696
 
660
 
    def is_creation(self, reverse=False):
 
697
    def is_creation(self, reverse):
661
698
        """Return true if applying the entry would create a file/directory.
662
699
 
663
700
        :param reverse: if true, the changeset is being applied in reverse
676
713
 
677
714
        :rtype: bool
678
715
        """
679
 
        return self.is_creation() or self.is_deletion()
 
716
        return self.is_creation(False) or self.is_deletion(False)
680
717
 
681
718
    def get_cset_path(self, mod=False):
682
719
        """Determine the path of the entry according to the changeset.
702
739
                return None
703
740
            return self.path
704
741
 
705
 
    def summarize_name(self):
 
742
    def summarize_name(self, reverse=False):
706
743
        """Produce a one-line summary of the filename.  Indicates renames as
707
744
        old => new, indicates creation as None => new, indicates deletion as
708
745
        old => None.
709
746
 
 
747
        :param changeset: The changeset to get paths from
 
748
        :type changeset: `Changeset`
 
749
        :param reverse: If true, reverse the names in the output
 
750
        :type reverse: bool
710
751
        :rtype: str
711
752
        """
712
753
        orig_path = self.get_cset_path(False)
718
759
        if orig_path == mod_path:
719
760
            return orig_path
720
761
        else:
721
 
            return "%s => %s" % (orig_path, mod_path)
722
 
 
723
 
    def get_new_path(self, id_map, changeset):
 
762
            if not reverse:
 
763
                return "%s => %s" % (orig_path, mod_path)
 
764
            else:
 
765
                return "%s => %s" % (mod_path, orig_path)
 
766
 
 
767
 
 
768
    def get_new_path(self, id_map, changeset, reverse=False):
724
769
        """Determine the full pathname to rename to
725
770
 
726
771
        :param id_map: The map of ids to filenames for the tree
727
772
        :type id_map: Dictionary
728
773
        :param changeset: The changeset to get data from
729
774
        :type changeset: `Changeset`
 
775
        :param reverse: If true, we're applying the changeset in reverse
 
776
        :type reverse: bool
730
777
        :rtype: str
731
778
        """
732
779
        mutter("Finding new path for %s", self.summarize_name())
733
 
        parent = self.new_parent
734
 
        to_dir = self.new_dir
735
 
        from_dir = self.dir
736
 
        to_name = self.new_name
737
 
        from_name = self.name
 
780
        if reverse:
 
781
            parent = self.parent
 
782
            to_dir = self.dir
 
783
            from_dir = self.new_dir
 
784
            to_name = self.name
 
785
            from_name = self.new_name
 
786
        else:
 
787
            parent = self.new_parent
 
788
            to_dir = self.new_dir
 
789
            from_dir = self.dir
 
790
            to_name = self.new_name
 
791
            from_name = self.name
738
792
 
739
793
        if to_name is None:
740
794
            return None
749
803
            dir = os.path.dirname(id_map[self.id])
750
804
        else:
751
805
            mutter("path, new_path: %r %r", self.path, self.new_path)
752
 
            dir = parent_entry.get_new_path(id_map, changeset)
 
806
            dir = parent_entry.get_new_path(id_map, changeset, reverse)
753
807
        if from_name == to_name:
754
808
            name = os.path.basename(id_map[self.id])
755
809
        else:
756
810
            name = to_name
757
811
            assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
758
 
        return pathjoin(dir, name)
 
812
        return os.path.join(dir, name)
759
813
 
760
814
    def is_boring(self):
761
815
        """Determines whether the entry does nothing
774
828
        else:
775
829
            return True
776
830
 
777
 
    def apply(self, filename, conflict_handler):
 
831
    def apply(self, filename, conflict_handler, reverse=False):
778
832
        """Applies the file content and/or metadata changes.
779
833
 
780
834
        :param filename: the filename of the entry
781
835
        :type filename: str
 
836
        :param reverse: If true, apply the changes in reverse
 
837
        :type reverse: bool
782
838
        """
783
 
        if self.is_deletion() and self.metadata_change is not None:
784
 
            self.metadata_change.apply(filename, conflict_handler)
 
839
        if self.is_deletion(reverse) and self.metadata_change is not None:
 
840
            self.metadata_change.apply(filename, conflict_handler, reverse)
785
841
        if self.contents_change is not None:
786
 
            self.contents_change.apply(filename, conflict_handler)
787
 
        if not self.is_deletion() and self.metadata_change is not None:
788
 
            self.metadata_change.apply(filename, conflict_handler)
789
 
 
 
842
            self.contents_change.apply(filename, conflict_handler, reverse)
 
843
        if not self.is_deletion(reverse) and self.metadata_change is not None:
 
844
            self.metadata_change.apply(filename, conflict_handler, reverse)
790
845
 
791
846
class IDPresent(Exception):
792
847
    def __init__(self, id):
795
850
        Exception.__init__(self, msg)
796
851
        self.id = id
797
852
 
798
 
 
799
853
class Changeset(object):
800
854
    """A set of changes to apply"""
801
855
    def __init__(self):
807
861
            raise IDPresent(entry.id)
808
862
        self.entries[entry.id] = entry
809
863
 
 
864
def my_sort(sequence, key, reverse=False):
 
865
    """A sort function that supports supplying a key for comparison
 
866
    
 
867
    :param sequence: The sequence to sort
 
868
    :param key: A callable object that returns the values to be compared
 
869
    :param reverse: If true, sort in reverse order
 
870
    :type reverse: bool
 
871
    """
 
872
    def cmp_by_key(entry_a, entry_b):
 
873
        if reverse:
 
874
            tmp=entry_a
 
875
            entry_a = entry_b
 
876
            entry_b = tmp
 
877
        return cmp(key(entry_a), key(entry_b))
 
878
    sequence.sort(cmp_by_key)
810
879
 
811
 
def get_rename_entries(changeset, inventory):
 
880
def get_rename_entries(changeset, inventory, reverse):
812
881
    """Return a list of entries that will be renamed.  Entries are sorted from
813
882
    longest to shortest source path and from shortest to longest target path.
814
883
 
816
885
    :type changeset: `Changeset`
817
886
    :param inventory: The source of current tree paths for the given ids
818
887
    :type inventory: Dictionary
 
888
    :param reverse: If true, the changeset is being applied in reverse
 
889
    :type reverse: bool
819
890
    :return: source entries and target entries as a tuple
820
891
    :rtype: (List, List)
821
892
    """
829
900
            return 0
830
901
        else:
831
902
            return len(path)
832
 
    source_entries.sort(None, longest_to_shortest, True)
 
903
    my_sort(source_entries, longest_to_shortest, reverse=True)
833
904
 
834
905
    target_entries = source_entries[:]
835
906
    # These are done from shortest to longest path, to avoid creating a
836
907
    # child before its parent has been created/renamed
837
908
    def shortest_to_longest(entry):
838
 
        path = entry.get_new_path(inventory, changeset)
 
909
        path = entry.get_new_path(inventory, changeset, reverse)
839
910
        if path is None:
840
911
            return 0
841
912
        else:
842
913
            return len(path)
843
 
    target_entries.sort(None, shortest_to_longest)
 
914
    my_sort(target_entries, shortest_to_longest)
844
915
    return (source_entries, target_entries)
845
916
 
846
 
 
847
917
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir, 
848
 
                          conflict_handler):
 
918
                          conflict_handler, reverse):
849
919
    """Delete and rename entries as appropriate.  Entries are renamed to temp
850
920
    names.  A map of id -> temp name (or None, for deletions) is returned.
851
921
 
855
925
    :type inventory: Dictionary
856
926
    :param dir: The directory to apply changes to
857
927
    :type dir: str
 
928
    :param reverse: Apply changes in reverse
 
929
    :type reverse: bool
858
930
    :return: a mapping of id to temporary name
859
931
    :rtype: Dictionary
860
932
    """
861
933
    temp_name = {}
862
934
    for i in range(len(source_entries)):
863
935
        entry = source_entries[i]
864
 
        if entry.is_deletion():
865
 
            path = pathjoin(dir, inventory[entry.id])
866
 
            entry.apply(path, conflict_handler)
 
936
        if entry.is_deletion(reverse):
 
937
            path = os.path.join(dir, inventory[entry.id])
 
938
            entry.apply(path, conflict_handler, reverse)
867
939
            temp_name[entry.id] = None
868
940
 
869
941
        elif entry.needs_rename():
870
 
            if entry.is_creation():
 
942
            if entry.is_creation(reverse):
871
943
                continue
872
 
            to_name = pathjoin(temp_dir, str(i))
 
944
            to_name = os.path.join(temp_dir, str(i))
873
945
            src_path = inventory.get(entry.id)
874
946
            if src_path is not None:
875
 
                src_path = pathjoin(dir, src_path)
 
947
                src_path = os.path.join(dir, src_path)
876
948
                try:
877
949
                    rename(src_path, to_name)
878
950
                    temp_name[entry.id] = to_name
887
959
 
888
960
 
889
961
def rename_to_new_create(changed_inventory, target_entries, inventory, 
890
 
                         changeset, dir, conflict_handler):
 
962
                         changeset, dir, conflict_handler, reverse):
891
963
    """Rename entries with temp names to their final names, create new files.
892
964
 
893
965
    :param changed_inventory: A mapping of id to temporary name
898
970
    :type changeset: `Changeset`
899
971
    :param dir: The directory to apply changes to
900
972
    :type dir: str
 
973
    :param reverse: If true, apply changes in reverse
 
974
    :type reverse: bool
901
975
    """
902
976
    for entry in target_entries:
903
 
        new_tree_path = entry.get_new_path(inventory, changeset)
 
977
        new_tree_path = entry.get_new_path(inventory, changeset, reverse)
904
978
        if new_tree_path is None:
905
979
            continue
906
 
        new_path = pathjoin(dir, new_tree_path)
 
980
        new_path = os.path.join(dir, new_tree_path)
907
981
        old_path = changed_inventory.get(entry.id)
908
982
        if bzrlib.osutils.lexists(new_path):
909
983
            if conflict_handler.target_exists(entry, new_path, old_path) == \
910
984
                "skip":
911
985
                continue
912
 
        if entry.is_creation():
913
 
            entry.apply(new_path, conflict_handler)
 
986
        if entry.is_creation(reverse):
 
987
            entry.apply(new_path, conflict_handler, reverse)
914
988
            changed_inventory[entry.id] = new_tree_path
915
989
        elif entry.needs_rename():
916
 
            if entry.is_deletion():
 
990
            if entry.is_deletion(reverse):
917
991
                continue
918
992
            if old_path is None:
919
993
                continue
925
999
                raise BzrCheckError('failed to rename %s to %s for changeset entry %s: %s'
926
1000
                        % (old_path, new_path, entry, e))
927
1001
 
928
 
 
929
1002
class TargetExists(Exception):
930
1003
    def __init__(self, entry, target):
931
1004
        msg = "The path %s already exists" % target
933
1006
        self.entry = entry
934
1007
        self.target = target
935
1008
 
936
 
 
937
1009
class RenameConflict(Exception):
938
1010
    def __init__(self, id, this_name, base_name, other_name):
939
1011
        msg = """Trees all have different names for a file
946
1018
        self.base_name = base_name
947
1019
        self_other_name = other_name
948
1020
 
949
 
 
950
1021
class MoveConflict(Exception):
951
1022
    def __init__(self, id, this_parent, base_parent, other_parent):
952
1023
        msg = """The file is in different directories in every tree
959
1030
        self.base_parent = base_parent
960
1031
        self_other_parent = other_parent
961
1032
 
962
 
 
963
1033
class MergeConflict(Exception):
964
1034
    def __init__(self, this_path):
965
1035
        Exception.__init__(self, "Conflict applying changes to %s" % this_path)
966
1036
        self.this_path = this_path
967
1037
 
968
 
 
969
1038
class WrongOldContents(Exception):
970
1039
    def __init__(self, filename):
971
1040
        msg = "Contents mismatch deleting %s" % filename
972
1041
        self.filename = filename
973
1042
        Exception.__init__(self, msg)
974
1043
 
975
 
 
976
1044
class WrongOldExecFlag(Exception):
977
1045
    def __init__(self, filename, old_exec_flag, new_exec_flag):
978
1046
        msg = "Executable flag missmatch on %s:\n" \
980
1048
        self.filename = filename
981
1049
        Exception.__init__(self, msg)
982
1050
 
983
 
 
984
1051
class RemoveContentsConflict(Exception):
985
1052
    def __init__(self, filename):
986
1053
        msg = "Conflict deleting %s, which has different contents in BASE"\
988
1055
        self.filename = filename
989
1056
        Exception.__init__(self, msg)
990
1057
 
991
 
 
992
1058
class DeletingNonEmptyDirectory(Exception):
993
1059
    def __init__(self, filename):
994
1060
        msg = "Trying to remove dir %s while it still had files" % filename
1002
1068
        Exception.__init__(self, msg)
1003
1069
        self.filename = filename
1004
1070
 
1005
 
 
1006
1071
class MissingForSetExec(Exception):
1007
1072
    def __init__(self, filename):
1008
1073
        msg = "Attempt to change permissions on  %s, which does not exist" %\
1010
1075
        Exception.__init__(self, msg)
1011
1076
        self.filename = filename
1012
1077
 
1013
 
 
1014
1078
class MissingForRm(Exception):
1015
1079
    def __init__(self, filename):
1016
1080
        msg = "Attempt to remove missing path %s" % filename
1024
1088
        Exception.__init__(self, msg)
1025
1089
        self.filename = filename
1026
1090
 
1027
 
 
1028
1091
class NewContentsConflict(Exception):
1029
1092
    def __init__(self, filename):
1030
1093
        msg = "Conflicting contents for new file %s" % (filename)
1031
1094
        Exception.__init__(self, msg)
1032
1095
 
1033
 
 
1034
1096
class WeaveMergeConflict(Exception):
1035
1097
    def __init__(self, filename):
1036
1098
        msg = "Conflicting contents for file %s" % (filename)
1037
1099
        Exception.__init__(self, msg)
1038
1100
 
1039
 
 
1040
1101
class ThreewayContentsConflict(Exception):
1041
1102
    def __init__(self, filename):
1042
1103
        msg = "Conflicting contents for file %s" % (filename)
1123
1184
    def finalize(self):
1124
1185
        pass
1125
1186
 
1126
 
 
1127
 
def apply_changeset(changeset, inventory, dir, conflict_handler=None):
 
1187
def apply_changeset(changeset, inventory, dir, conflict_handler=None, 
 
1188
                    reverse=False):
1128
1189
    """Apply a changeset to a directory.
1129
1190
 
1130
1191
    :param changeset: The changes to perform
1133
1194
    :type inventory: Dictionary
1134
1195
    :param dir: The path of the directory to apply the changes to
1135
1196
    :type dir: str
 
1197
    :param reverse: If true, apply the changes in reverse
 
1198
    :type reverse: bool
1136
1199
    :return: The mapping of the changed entries
1137
1200
    :rtype: Dictionary
1138
1201
    """
1139
1202
    if conflict_handler is None:
1140
1203
        conflict_handler = ExceptionConflictHandler()
1141
 
    temp_dir = pathjoin(dir, "bzr-tree-change")
 
1204
    temp_dir = os.path.join(dir, "bzr-tree-change")
1142
1205
    try:
1143
1206
        os.mkdir(temp_dir)
1144
1207
    except OSError, e:
1159
1222
                warning("entry {%s} no longer present, can't be updated",
1160
1223
                        entry.id)
1161
1224
                continue
1162
 
            path = pathjoin(dir, inventory[entry.id])
1163
 
            entry.apply(path, conflict_handler)
 
1225
            path = os.path.join(dir, inventory[entry.id])
 
1226
            entry.apply(path, conflict_handler, reverse)
1164
1227
 
1165
1228
    # Apply renames in stages, to minimize conflicts:
1166
1229
    # Only files whose name or parent change are interesting, because their
1167
1230
    # target name may exist in the source tree.  If a directory's name changes,
1168
1231
    # that doesn't make its children interesting.
1169
 
    (source_entries, target_entries) = get_rename_entries(changeset, inventory)
 
1232
    (source_entries, target_entries) = get_rename_entries(changeset, inventory,
 
1233
                                                          reverse)
1170
1234
 
1171
1235
    changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1172
 
                                              temp_dir, conflict_handler)
 
1236
                                              temp_dir, conflict_handler,
 
1237
                                              reverse)
1173
1238
 
1174
1239
    rename_to_new_create(changed_inventory, target_entries, inventory,
1175
 
                         changeset, dir, conflict_handler)
 
1240
                         changeset, dir, conflict_handler, reverse)
1176
1241
    os.rmdir(temp_dir)
1177
1242
    return changed_inventory
1178
1243
 
1179
1244
 
 
1245
def apply_changeset_tree(cset, tree, reverse=False):
 
1246
    r_inventory = {}
 
1247
    for entry in tree.source_inventory().itervalues():
 
1248
        inventory[entry.id] = entry.path
 
1249
    new_inventory = apply_changeset(cset, r_inventory, tree.basedir,
 
1250
                                    reverse=reverse)
 
1251
    new_entries, remove_entries = \
 
1252
        get_inventory_change(inventory, new_inventory, cset, reverse)
 
1253
    tree.update_source_inventory(new_entries, remove_entries)
 
1254
 
 
1255
 
 
1256
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
 
1257
    new_entries = {}
 
1258
    remove_entries = []
 
1259
    for entry in cset.entries.itervalues():
 
1260
        if entry.needs_rename():
 
1261
            new_path = entry.get_new_path(inventory, cset)
 
1262
            if new_path is None:
 
1263
                remove_entries.append(entry.id)
 
1264
            else:
 
1265
                new_entries[new_path] = entry.id
 
1266
    return new_entries, remove_entries
 
1267
 
 
1268
 
1180
1269
def print_changeset(cset):
1181
1270
    """Print all non-boring changeset entries
1182
1271
    
1189
1278
        print entry.id
1190
1279
        print entry.summarize_name(cset)
1191
1280
 
 
1281
class CompositionFailure(Exception):
 
1282
    def __init__(self, old_entry, new_entry, problem):
 
1283
        msg = "Unable to conpose entries.\n %s" % problem
 
1284
        Exception.__init__(self, msg)
 
1285
 
 
1286
class IDMismatch(CompositionFailure):
 
1287
    def __init__(self, old_entry, new_entry):
 
1288
        problem = "Attempt to compose entries with different ids: %s and %s" %\
 
1289
            (old_entry.id, new_entry.id)
 
1290
        CompositionFailure.__init__(self, old_entry, new_entry, problem)
 
1291
 
 
1292
def compose_changesets(old_cset, new_cset):
 
1293
    """Combine two changesets into one.  This works well for exact patching.
 
1294
    Otherwise, not so well.
 
1295
 
 
1296
    :param old_cset: The first changeset that would be applied
 
1297
    :type old_cset: `Changeset`
 
1298
    :param new_cset: The second changeset that would be applied
 
1299
    :type new_cset: `Changeset`
 
1300
    :return: A changeset that combines the changes in both changesets
 
1301
    :rtype: `Changeset`
 
1302
    """
 
1303
    composed = Changeset()
 
1304
    for old_entry in old_cset.entries.itervalues():
 
1305
        new_entry = new_cset.entries.get(old_entry.id)
 
1306
        if new_entry is None:
 
1307
            composed.add_entry(old_entry)
 
1308
        else:
 
1309
            composed_entry = compose_entries(old_entry, new_entry)
 
1310
            if composed_entry.parent is not None or\
 
1311
                composed_entry.new_parent is not None:
 
1312
                composed.add_entry(composed_entry)
 
1313
    for new_entry in new_cset.entries.itervalues():
 
1314
        if not old_cset.entries.has_key(new_entry.id):
 
1315
            composed.add_entry(new_entry)
 
1316
    return composed
 
1317
 
 
1318
def compose_entries(old_entry, new_entry):
 
1319
    """Combine two entries into one.
 
1320
 
 
1321
    :param old_entry: The first entry that would be applied
 
1322
    :type old_entry: ChangesetEntry
 
1323
    :param old_entry: The second entry that would be applied
 
1324
    :type old_entry: ChangesetEntry
 
1325
    :return: A changeset entry combining both entries
 
1326
    :rtype: `ChangesetEntry`
 
1327
    """
 
1328
    if old_entry.id != new_entry.id:
 
1329
        raise IDMismatch(old_entry, new_entry)
 
1330
    output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
 
1331
 
 
1332
    if (old_entry.parent != old_entry.new_parent or 
 
1333
        new_entry.parent != new_entry.new_parent):
 
1334
        output.new_parent = new_entry.new_parent
 
1335
 
 
1336
    if (old_entry.path != old_entry.new_path or 
 
1337
        new_entry.path != new_entry.new_path):
 
1338
        output.new_path = new_entry.new_path
 
1339
 
 
1340
    output.contents_change = compose_contents(old_entry, new_entry)
 
1341
    output.metadata_change = compose_metadata(old_entry, new_entry)
 
1342
    return output
 
1343
 
 
1344
def compose_contents(old_entry, new_entry):
 
1345
    """Combine the contents of two changeset entries.  Entries are combined
 
1346
    intelligently where possible, but the fallback behavior returns an 
 
1347
    ApplySequence.
 
1348
 
 
1349
    :param old_entry: The first entry that would be applied
 
1350
    :type old_entry: `ChangesetEntry`
 
1351
    :param new_entry: The second entry that would be applied
 
1352
    :type new_entry: `ChangesetEntry`
 
1353
    :return: A combined contents change
 
1354
    :rtype: anything supporting the apply(reverse=False) method
 
1355
    """
 
1356
    old_contents = old_entry.contents_change
 
1357
    new_contents = new_entry.contents_change
 
1358
    if old_entry.contents_change is None:
 
1359
        return new_entry.contents_change
 
1360
    elif new_entry.contents_change is None:
 
1361
        return old_entry.contents_change
 
1362
    elif isinstance(old_contents, ReplaceContents) and \
 
1363
        isinstance(new_contents, ReplaceContents):
 
1364
        if old_contents.old_contents == new_contents.new_contents:
 
1365
            return None
 
1366
        else:
 
1367
            return ReplaceContents(old_contents.old_contents,
 
1368
                                   new_contents.new_contents)
 
1369
    elif isinstance(old_contents, ApplySequence):
 
1370
        output = ApplySequence(old_contents.changes)
 
1371
        if isinstance(new_contents, ApplySequence):
 
1372
            output.changes.extend(new_contents.changes)
 
1373
        else:
 
1374
            output.changes.append(new_contents)
 
1375
        return output
 
1376
    elif isinstance(new_contents, ApplySequence):
 
1377
        output = ApplySequence((old_contents.changes,))
 
1378
        output.extend(new_contents.changes)
 
1379
        return output
 
1380
    else:
 
1381
        return ApplySequence((old_contents, new_contents))
 
1382
 
 
1383
def compose_metadata(old_entry, new_entry):
 
1384
    old_meta = old_entry.metadata_change
 
1385
    new_meta = new_entry.metadata_change
 
1386
    if old_meta is None:
 
1387
        return new_meta
 
1388
    elif new_meta is None:
 
1389
        return old_meta
 
1390
    elif (isinstance(old_meta, ChangeExecFlag) and
 
1391
          isinstance(new_meta, ChangeExecFlag)):
 
1392
        return ChangeExecFlag(old_meta.old_exec_flag, new_meta.new_exec_flag)
 
1393
    else:
 
1394
        return ApplySequence(old_meta, new_meta)
 
1395
 
 
1396
 
 
1397
def changeset_is_null(changeset):
 
1398
    for entry in changeset.entries.itervalues():
 
1399
        if not entry.is_boring():
 
1400
            return False
 
1401
    return True
1192
1402
 
1193
1403
class UnsupportedFiletype(Exception):
1194
1404
    def __init__(self, kind, full_path):
1198
1408
        self.full_path = full_path
1199
1409
        self.kind = kind
1200
1410
 
1201
 
 
1202
1411
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1203
1412
    return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1204
1413
 
1293
1502
            return self.make_entry(id, only_interesting=False)
1294
1503
        else:
1295
1504
            return cs_entry
 
1505
        
1296
1506
 
1297
1507
    def make_entry(self, id, only_interesting=True):
1298
1508
        cs_entry = self.make_basic_entry(id, only_interesting)
1347
1557
 
1348
1558
 
1349
1559
def full_path(entry, tree):
1350
 
    return pathjoin(tree.basedir, entry.path)
1351
 
 
 
1560
    return os.path.join(tree.basedir, entry.path)
1352
1561
 
1353
1562
def new_delete_entry(entry, tree, inventory, delete):
1354
1563
    if entry.path == "":
1366
1575
    status = os.lstat(full_path)
1367
1576
    if stat.S_ISDIR(file_stat.st_mode):
1368
1577
        action = dir_create
1369
 
 
1370
 
 
 
1578
    
 
1579
 
 
1580
 
 
1581
        
1371
1582
# XXX: Can't we unify this with the regular inventory object
1372
1583
class Inventory(object):
1373
1584
    def __init__(self, inventory):
1406
1617
        if directory is None:
1407
1618
            return NULL_ID
1408
1619
        return self.get_rinventory().get(directory)
1409
 
 
1410