~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/changeset.py

Merge from integration.

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