~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/changeset.py

[merge] jam-integration 1495

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