~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/changeset.py

  • Committer: Martin Pool
  • Date: 2005-09-06 07:26:13 UTC
  • Revision ID: mbp@sourcefrog.net-20050906072613-1a4a18769aaaa3eb
- add xml round-trip test for revisions

- fix up __eq__ method for Revision

Show diffs side-by-side

added added

removed removed

Lines of Context:
13
13
#    You should have received a copy of the GNU General Public License
14
14
#    along with this program; if not, write to the Free Software
15
15
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
"""Represent and apply a changeset.
18
 
 
19
 
Conflicts in applying a changeset are represented as exceptions.
20
 
 
21
 
This only handles the in-memory objects representing changesets, which are
22
 
primarily used by the merge code. 
23
 
"""
24
 
 
25
16
import os.path
26
17
import errno
 
18
import patch
27
19
import stat
28
 
from tempfile import mkdtemp
29
 
from shutil import rmtree
30
 
from itertools import izip
31
 
 
32
 
from bzrlib.trace import mutter, warning
33
 
from bzrlib.osutils import rename, sha_file
34
 
import bzrlib
35
 
from bzrlib.errors import BzrCheckError
 
20
from bzrlib.trace import mutter
 
21
 
 
22
# XXX: mbp: I'm not totally convinced that we should handle conflicts
 
23
# as part of changeset application, rather than only in the merge
 
24
# operation.
 
25
 
 
26
"""Represent and apply a changeset
 
27
 
 
28
Conflicts in applying a changeset are represented as exceptions.
 
29
"""
36
30
 
37
31
__docformat__ = "restructuredtext"
38
32
 
48
42
        newdict[value] = key
49
43
    return newdict
50
44
 
 
45
 
51
46
       
52
 
class ChangeExecFlag(object):
 
47
class ChangeUnixPermissions(object):
53
48
    """This is two-way change, suitable for file modification, creation,
54
49
    deletion"""
55
 
    def __init__(self, old_exec_flag, new_exec_flag):
56
 
        self.old_exec_flag = old_exec_flag
57
 
        self.new_exec_flag = new_exec_flag
 
50
    def __init__(self, old_mode, new_mode):
 
51
        self.old_mode = old_mode
 
52
        self.new_mode = new_mode
58
53
 
59
54
    def apply(self, filename, conflict_handler, reverse=False):
60
55
        if not reverse:
61
 
            from_exec_flag = self.old_exec_flag
62
 
            to_exec_flag = self.new_exec_flag
 
56
            from_mode = self.old_mode
 
57
            to_mode = self.new_mode
63
58
        else:
64
 
            from_exec_flag = self.new_exec_flag
65
 
            to_exec_flag = self.old_exec_flag
 
59
            from_mode = self.new_mode
 
60
            to_mode = self.old_mode
66
61
        try:
67
 
            current_exec_flag = bool(os.stat(filename).st_mode & 0111)
 
62
            current_mode = os.stat(filename).st_mode &0777
68
63
        except OSError, e:
69
64
            if e.errno == errno.ENOENT:
70
 
                if conflict_handler.missing_for_exec_flag(filename) == "skip":
 
65
                if conflict_handler.missing_for_chmod(filename) == "skip":
71
66
                    return
72
67
                else:
73
 
                    current_exec_flag = from_exec_flag
 
68
                    current_mode = from_mode
74
69
 
75
 
        if from_exec_flag is not None and current_exec_flag != from_exec_flag:
76
 
            if conflict_handler.wrong_old_exec_flag(filename,
77
 
                        from_exec_flag, current_exec_flag) != "continue":
 
70
        if from_mode is not None and current_mode != from_mode:
 
71
            if conflict_handler.wrong_old_perms(filename, from_mode, 
 
72
                                                current_mode) != "continue":
78
73
                return
79
74
 
80
 
        if to_exec_flag is not None:
81
 
            current_mode = os.stat(filename).st_mode
82
 
            if to_exec_flag:
83
 
                umask = os.umask(0)
84
 
                os.umask(umask)
85
 
                to_mode = current_mode | (0100 & ~umask)
86
 
                # Enable x-bit for others only if they can read it.
87
 
                if current_mode & 0004:
88
 
                    to_mode |= 0001 & ~umask
89
 
                if current_mode & 0040:
90
 
                    to_mode |= 0010 & ~umask
91
 
            else:
92
 
                to_mode = current_mode & ~0111
 
75
        if to_mode is not None:
93
76
            try:
94
77
                os.chmod(filename, to_mode)
95
78
            except IOError, e:
96
79
                if e.errno == errno.ENOENT:
97
 
                    conflict_handler.missing_for_exec_flag(filename)
 
80
                    conflict_handler.missing_for_chmod(filename)
98
81
 
99
82
    def __eq__(self, other):
100
 
        return (isinstance(other, ChangeExecFlag) and
101
 
                self.old_exec_flag == other.old_exec_flag and
102
 
                self.new_exec_flag == other.new_exec_flag)
 
83
        if not isinstance(other, ChangeUnixPermissions):
 
84
            return False
 
85
        elif self.old_mode != other.old_mode:
 
86
            return False
 
87
        elif self.new_mode != other.new_mode:
 
88
            return False
 
89
        else:
 
90
            return True
103
91
 
104
92
    def __ne__(self, other):
105
93
        return not (self == other)
106
94
 
107
 
 
108
95
def dir_create(filename, conflict_handler, reverse):
109
96
    """Creates the directory, or deletes it if reverse is true.  Intended to be
110
97
    used with ReplaceContents.
130
117
        try:
131
118
            os.rmdir(filename)
132
119
        except OSError, e:
133
 
            if e.errno != errno.ENOTEMPTY:
 
120
            if e.errno != 39:
134
121
                raise
135
122
            if conflict_handler.rmdir_non_empty(filename) == "skip":
136
123
                return
137
124
            os.rmdir(filename)
138
125
 
 
126
                
 
127
            
139
128
 
140
129
class SymlinkCreate(object):
141
130
    """Creates or deletes a symlink (for use with ReplaceContents)"""
147
136
        """
148
137
        self.target = contents
149
138
 
150
 
    def __repr__(self):
151
 
        return "SymlinkCreate(%s)" % self.target
152
 
 
153
139
    def __call__(self, filename, conflict_handler, reverse):
154
140
        """Creates or destroys the symlink.
155
141
 
237
223
 
238
224
                    
239
225
 
240
 
class TreeFileCreate(object):
241
 
    """Create or delete a file (for use with ReplaceContents)"""
242
 
    def __init__(self, tree, file_id):
243
 
        """Constructor
244
 
 
245
 
        :param contents: The contents of the file to write
246
 
        :type contents: str
247
 
        """
248
 
        self.tree = tree
249
 
        self.file_id = file_id
250
 
 
251
 
    def __repr__(self):
252
 
        return "TreeFileCreate(%s)" % self.file_id
253
 
 
254
 
    def __eq__(self, other):
255
 
        if not isinstance(other, TreeFileCreate):
256
 
            return False
257
 
        return self.tree.get_file_sha1(self.file_id) == \
258
 
            other.tree.get_file_sha1(other.file_id)
259
 
 
260
 
    def __ne__(self, other):
261
 
        return not (self == other)
262
 
 
263
 
    def write_file(self, filename):
264
 
        outfile = file(filename, "wb")
265
 
        for line in self.tree.get_file(self.file_id):
266
 
            outfile.write(line)
267
 
 
268
 
    def same_text(self, filename):
269
 
        in_file = file(filename, "rb")
270
 
        return sha_file(in_file) == self.tree.get_file_sha1(self.file_id)
271
 
 
272
 
    def __call__(self, filename, conflict_handler, reverse):
273
 
        """Create or delete a file
274
 
 
275
 
        :param filename: The name of the file to create
276
 
        :type filename: str
277
 
        :param reverse: Delete the file instead of creating it
278
 
        :type reverse: bool
279
 
        """
280
 
        if not reverse:
281
 
            try:
282
 
                self.write_file(filename)
283
 
            except IOError, e:
284
 
                if e.errno == errno.ENOENT:
285
 
                    if conflict_handler.missing_parent(filename)=="continue":
286
 
                        self.write_file(filename)
287
 
                else:
288
 
                    raise
289
 
 
290
 
        else:
291
 
            try:
292
 
                if not self.same_text(filename):
293
 
                    direction = conflict_handler.wrong_old_contents(filename,
294
 
                        self.tree.get_file(self.file_id).read())
295
 
                    if  direction != "continue":
296
 
                        return
297
 
                os.unlink(filename)
298
 
            except IOError, e:
299
 
                if e.errno != errno.ENOENT:
300
 
                    raise
301
 
                if conflict_handler.missing_for_rm(filename, undo) == "skip":
302
 
                    return
303
 
 
304
 
                    
305
 
 
306
226
def reversed(sequence):
307
227
    max = len(sequence) - 1
308
228
    for i in range(len(sequence)):
375
295
            if mode is not None:
376
296
                os.chmod(filename, mode)
377
297
 
378
 
    def is_creation(self):
379
 
        return self.new_contents is not None and self.old_contents is None
380
 
 
381
 
    def is_deletion(self):
382
 
        return self.old_contents is not None and self.new_contents is None
383
 
 
384
298
class ApplySequence(object):
385
299
    def __init__(self, changes=None):
386
300
        self.changes = []
412
326
 
413
327
 
414
328
class Diff3Merge(object):
415
 
    history_based = False
416
329
    def __init__(self, file_id, base, other):
417
330
        self.file_id = file_id
418
331
        self.base = base
419
332
        self.other = other
420
333
 
421
 
    def is_creation(self):
422
 
        return False
423
 
 
424
 
    def is_deletion(self):
425
 
        return False
426
 
 
427
334
    def __eq__(self, other):
428
335
        if not isinstance(other, Diff3Merge):
429
336
            return False
433
340
    def __ne__(self, other):
434
341
        return not (self == other)
435
342
 
436
 
    def dump_file(self, temp_dir, name, tree):
437
 
        out_path = os.path.join(temp_dir, name)
438
 
        out_file = file(out_path, "wb")
439
 
        in_file = tree.get_file(self.file_id)
440
 
        for line in in_file:
441
 
            out_file.write(line)
442
 
        return out_path
443
 
 
444
343
    def apply(self, filename, conflict_handler, reverse=False):
445
 
        import bzrlib.patch
446
 
        temp_dir = mkdtemp(prefix="bzr-")
447
 
        try:
448
 
            new_file = filename+".new"
449
 
            base_file = self.dump_file(temp_dir, "base", self.base)
450
 
            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
457
 
            status = bzrlib.patch.diff3(new_file, filename, base, other)
458
 
            if status == 0:
459
 
                os.chmod(new_file, os.stat(filename).st_mode)
460
 
                rename(new_file, filename)
461
 
                return
462
 
            else:
463
 
                assert(status == 1)
464
 
                def get_lines(filename):
465
 
                    my_file = file(filename, "rb")
466
 
                    lines = my_file.readlines()
467
 
                    my_file.close()
468
 
                    return lines
469
 
                base_lines = get_lines(base)
470
 
                other_lines = get_lines(other)
471
 
                conflict_handler.merge_conflict(new_file, filename, base_lines, 
472
 
                                                other_lines)
473
 
        finally:
474
 
            rmtree(temp_dir)
 
344
        new_file = filename+".new"
 
345
        base_file = self.base.readonly_path(self.file_id)
 
346
        other_file = self.other.readonly_path(self.file_id)
 
347
        if not reverse:
 
348
            base = base_file
 
349
            other = other_file
 
350
        else:
 
351
            base = other_file
 
352
            other = base_file
 
353
        status = patch.diff3(new_file, filename, base, other)
 
354
        if status == 0:
 
355
            os.chmod(new_file, os.stat(filename).st_mode)
 
356
            os.rename(new_file, filename)
 
357
            return
 
358
        else:
 
359
            assert(status == 1)
 
360
            def get_lines(filename):
 
361
                my_file = file(base, "rb")
 
362
                lines = my_file.readlines()
 
363
                my_file.close()
 
364
            base_lines = get_lines(base)
 
365
            other_lines = get_lines(other)
 
366
            conflict_handler.merge_conflict(new_file, filename, base_lines, 
 
367
                                            other_lines)
475
368
 
476
369
 
477
370
def CreateDir():
510
403
    """
511
404
    return ReplaceContents(FileCreate(contents), None)
512
405
 
513
 
def ReplaceFileContents(old_tree, new_tree, file_id):
 
406
def ReplaceFileContents(old_contents, new_contents):
514
407
    """Convenience fucntion to replace the contents of a file.
515
408
    
516
409
    :param old_contents: The contents of the file to replace 
520
413
    :return: A ReplaceContents that will replace the contents of a file a file 
521
414
    :rtype: `ReplaceContents`
522
415
    """
523
 
    return ReplaceContents(TreeFileCreate(old_tree, file_id), 
524
 
                           TreeFileCreate(new_tree, file_id))
 
416
    return ReplaceContents(FileCreate(old_contents), FileCreate(new_contents))
525
417
 
526
418
def CreateSymlink(target):
527
419
    """Convenience fucntion to create a symlink.
633
525
        if self.id  == self.parent:
634
526
            raise ParentIDIsSelf(self)
635
527
 
636
 
    def __repr__(self):
 
528
    def __str__(self):
637
529
        return "ChangesetEntry(%s)" % self.id
638
530
 
639
 
    __str__ = __repr__
640
 
 
641
531
    def __get_dir(self):
642
532
        if self.path is None:
643
533
            return None
692
582
        :param reverse: if true, the changeset is being applied in reverse
693
583
        :rtype: bool
694
584
        """
695
 
        return self.is_creation(not reverse)
 
585
        return ((self.new_parent is None and not reverse) or 
 
586
                (self.parent is None and reverse))
696
587
 
697
588
    def is_creation(self, reverse):
698
589
        """Return true if applying the entry would create a file/directory.
700
591
        :param reverse: if true, the changeset is being applied in reverse
701
592
        :rtype: bool
702
593
        """
703
 
        if self.contents_change is None:
704
 
            return False
705
 
        if reverse:
706
 
            return self.contents_change.is_deletion()
707
 
        else:
708
 
            return self.contents_change.is_creation()
 
594
        return ((self.parent is None and not reverse) or 
 
595
                (self.new_parent is None and reverse))
709
596
 
710
597
    def is_creation_or_deletion(self):
711
598
        """Return true if applying the entry would create or delete a 
713
600
 
714
601
        :rtype: bool
715
602
        """
716
 
        return self.is_creation(False) or self.is_deletion(False)
 
603
        return self.parent is None or self.new_parent is None
717
604
 
718
605
    def get_cset_path(self, mod=False):
719
606
        """Determine the path of the entry according to the changeset.
752
639
        """
753
640
        orig_path = self.get_cset_path(False)
754
641
        mod_path = self.get_cset_path(True)
755
 
        if orig_path and orig_path.startswith('./'):
 
642
        if orig_path is not None:
756
643
            orig_path = orig_path[2:]
757
 
        if mod_path and mod_path.startswith('./'):
 
644
        if mod_path is not None:
758
645
            mod_path = mod_path[2:]
759
646
        if orig_path == mod_path:
760
647
            return orig_path
776
663
        :type reverse: bool
777
664
        :rtype: str
778
665
        """
779
 
        mutter("Finding new path for %s", self.summarize_name())
 
666
        mutter("Finding new path for %s" % self.summarize_name())
780
667
        if reverse:
781
668
            parent = self.parent
782
669
            to_dir = self.dir
794
681
            return None
795
682
 
796
683
        if parent == NULL_ID or parent is None:
797
 
            if to_name != u'.':
 
684
            if to_name != '.':
798
685
                raise SourceRootHasName(self, to_name)
799
686
            else:
800
 
                return u'.'
801
 
        parent_entry = changeset.entries.get(parent)
802
 
        if parent_entry is None:
 
687
                return '.'
 
688
        if from_dir == to_dir:
803
689
            dir = os.path.dirname(id_map[self.id])
804
690
        else:
805
 
            mutter("path, new_path: %r %r", self.path, self.new_path)
 
691
            mutter("path, new_path: %r %r" % (self.path, self.new_path))
 
692
            parent_entry = changeset.entries[parent]
806
693
            dir = parent_entry.get_new_path(id_map, changeset, reverse)
807
694
        if from_name == to_name:
808
695
            name = os.path.basename(id_map[self.id])
891
778
    :rtype: (List, List)
892
779
    """
893
780
    source_entries = [x for x in changeset.entries.itervalues() 
894
 
                      if x.needs_rename() or x.is_creation_or_deletion()]
 
781
                      if x.needs_rename()]
895
782
    # these are done from longest path to shortest, to avoid deleting a
896
783
    # parent before its children are deleted/renamed 
897
784
    def longest_to_shortest(entry):
938
825
            entry.apply(path, conflict_handler, reverse)
939
826
            temp_name[entry.id] = None
940
827
 
941
 
        elif entry.needs_rename():
942
 
            if entry.is_creation(reverse):
943
 
                continue
 
828
        else:
944
829
            to_name = os.path.join(temp_dir, str(i))
945
830
            src_path = inventory.get(entry.id)
946
831
            if src_path is not None:
947
832
                src_path = os.path.join(dir, src_path)
948
833
                try:
949
 
                    rename(src_path, to_name)
 
834
                    os.rename(src_path, to_name)
950
835
                    temp_name[entry.id] = to_name
951
836
                except OSError, e:
952
837
                    if e.errno != errno.ENOENT:
953
838
                        raise
954
 
                    if conflict_handler.missing_for_rename(src_path, to_name) \
955
 
                        == "skip":
 
839
                    if conflict_handler.missing_for_rename(src_path) == "skip":
956
840
                        continue
957
841
 
958
842
    return temp_name
979
863
            continue
980
864
        new_path = os.path.join(dir, new_tree_path)
981
865
        old_path = changed_inventory.get(entry.id)
982
 
        if bzrlib.osutils.lexists(new_path):
 
866
        if os.path.exists(new_path):
983
867
            if conflict_handler.target_exists(entry, new_path, old_path) == \
984
868
                "skip":
985
869
                continue
986
870
        if entry.is_creation(reverse):
987
871
            entry.apply(new_path, conflict_handler, reverse)
988
872
            changed_inventory[entry.id] = new_tree_path
989
 
        elif entry.needs_rename():
990
 
            if entry.is_deletion(reverse):
991
 
                continue
 
873
        else:
992
874
            if old_path is None:
993
875
                continue
994
876
            try:
995
 
                mutter('rename %s to final name %s', old_path, new_path)
996
 
                rename(old_path, new_path)
 
877
                os.rename(old_path, new_path)
997
878
                changed_inventory[entry.id] = new_tree_path
998
879
            except OSError, e:
999
 
                raise BzrCheckError('failed to rename %s to %s for changeset entry %s: %s'
1000
 
                        % (old_path, new_path, entry, e))
 
880
                raise Exception ("%s is missing" % new_path)
1001
881
 
1002
882
class TargetExists(Exception):
1003
883
    def __init__(self, entry, target):
1035
915
        Exception.__init__(self, "Conflict applying changes to %s" % this_path)
1036
916
        self.this_path = this_path
1037
917
 
 
918
class MergePermissionConflict(Exception):
 
919
    def __init__(self, this_path, base_path, other_path):
 
920
        this_perms = os.stat(this_path).st_mode & 0755
 
921
        base_perms = os.stat(base_path).st_mode & 0755
 
922
        other_perms = os.stat(other_path).st_mode & 0755
 
923
        msg = """Conflicting permission for %s
 
924
this: %o
 
925
base: %o
 
926
other: %o
 
927
        """ % (this_path, this_perms, base_perms, other_perms)
 
928
        self.this_path = this_path
 
929
        self.base_path = base_path
 
930
        self.other_path = other_path
 
931
        Exception.__init__(self, msg)
 
932
 
1038
933
class WrongOldContents(Exception):
1039
934
    def __init__(self, filename):
1040
935
        msg = "Contents mismatch deleting %s" % filename
1041
936
        self.filename = filename
1042
937
        Exception.__init__(self, msg)
1043
938
 
1044
 
class WrongOldExecFlag(Exception):
1045
 
    def __init__(self, filename, old_exec_flag, new_exec_flag):
1046
 
        msg = "Executable flag missmatch on %s:\n" \
1047
 
        "Expected %s, got %s." % (filename, old_exec_flag, new_exec_flag)
 
939
class WrongOldPermissions(Exception):
 
940
    def __init__(self, filename, old_perms, new_perms):
 
941
        msg = "Permission missmatch on %s:\n" \
 
942
        "Expected 0%o, got 0%o." % (filename, old_perms, new_perms)
1048
943
        self.filename = filename
1049
944
        Exception.__init__(self, msg)
1050
945
 
1068
963
        Exception.__init__(self, msg)
1069
964
        self.filename = filename
1070
965
 
1071
 
class MissingForSetExec(Exception):
 
966
class MissingPermsFile(Exception):
1072
967
    def __init__(self, filename):
1073
968
        msg = "Attempt to change permissions on  %s, which does not exist" %\
1074
969
            filename
1083
978
 
1084
979
 
1085
980
class MissingForRename(Exception):
1086
 
    def __init__(self, filename, to_path):
1087
 
        msg = "Attempt to move missing path %s to %s" % (filename, to_path)
 
981
    def __init__(self, filename):
 
982
        msg = "Attempt to move missing path %s" % (filename)
1088
983
        Exception.__init__(self, msg)
1089
984
        self.filename = filename
1090
985
 
1093
988
        msg = "Conflicting contents for new file %s" % (filename)
1094
989
        Exception.__init__(self, msg)
1095
990
 
1096
 
class WeaveMergeConflict(Exception):
1097
 
    def __init__(self, filename):
1098
 
        msg = "Conflicting contents for file %s" % (filename)
1099
 
        Exception.__init__(self, msg)
1100
 
 
1101
 
class ThreewayContentsConflict(Exception):
1102
 
    def __init__(self, filename):
1103
 
        msg = "Conflicting contents for file %s" % (filename)
1104
 
        Exception.__init__(self, msg)
1105
 
 
1106
991
 
1107
992
class MissingForMerge(Exception):
1108
993
    def __init__(self, filename):
1118
1003
    descend from this class if they have a better way to handle some or
1119
1004
    all types of conflict.
1120
1005
    """
 
1006
    def __init__(self, dir):
 
1007
        self.dir = dir
 
1008
    
1121
1009
    def missing_parent(self, pathname):
1122
1010
        parent = os.path.dirname(pathname)
1123
1011
        raise Exception("Parent directory missing for %s" % pathname)
1141
1029
        os.unlink(new_file)
1142
1030
        raise MergeConflict(this_path)
1143
1031
 
 
1032
    def permission_conflict(self, this_path, base_path, other_path):
 
1033
        raise MergePermissionConflict(this_path, base_path, other_path)
 
1034
 
1144
1035
    def wrong_old_contents(self, filename, expected_contents):
1145
1036
        raise WrongOldContents(filename)
1146
1037
 
1147
1038
    def rem_contents_conflict(self, filename, this_contents, base_contents):
1148
1039
        raise RemoveContentsConflict(filename)
1149
1040
 
1150
 
    def wrong_old_exec_flag(self, filename, old_exec_flag, new_exec_flag):
1151
 
        raise WrongOldExecFlag(filename, old_exec_flag, new_exec_flag)
 
1041
    def wrong_old_perms(self, filename, old_perms, new_perms):
 
1042
        raise WrongOldPermissions(filename, old_perms, new_perms)
1152
1043
 
1153
1044
    def rmdir_non_empty(self, filename):
1154
1045
        raise DeletingNonEmptyDirectory(filename)
1159
1050
    def patch_target_missing(self, filename, contents):
1160
1051
        raise PatchTargetMissing(filename)
1161
1052
 
1162
 
    def missing_for_exec_flag(self, filename):
1163
 
        raise MissingForExecFlag(filename)
 
1053
    def missing_for_chmod(self, filename):
 
1054
        raise MissingPermsFile(filename)
1164
1055
 
1165
1056
    def missing_for_rm(self, filename, change):
1166
1057
        raise MissingForRm(filename)
1167
1058
 
1168
 
    def missing_for_rename(self, filename, to_path):
1169
 
        raise MissingForRename(filename, to_path)
 
1059
    def missing_for_rename(self, filename):
 
1060
        raise MissingForRename(filename)
1170
1061
 
1171
1062
    def missing_for_merge(self, file_id, other_path):
1172
1063
        raise MissingForMerge(other_path)
1174
1065
    def new_contents_conflict(self, filename, other_contents):
1175
1066
        raise NewContentsConflict(filename)
1176
1067
 
1177
 
    def weave_merge_conflict(self, filename, weave, other_i, out_file):
1178
 
        raise WeaveMergeConflict(filename)
1179
 
 
1180
 
    def threeway_contents_conflict(self, filename, this_contents,
1181
 
                                   base_contents, other_contents):
1182
 
        raise ThreewayContentsConflict(filename)
1183
 
 
1184
1068
    def finalize(self):
1185
1069
        pass
1186
1070
 
1200
1084
    :rtype: Dictionary
1201
1085
    """
1202
1086
    if conflict_handler is None:
1203
 
        conflict_handler = ExceptionConflictHandler()
 
1087
        conflict_handler = ExceptionConflictHandler(dir)
1204
1088
    temp_dir = os.path.join(dir, "bzr-tree-change")
1205
1089
    try:
1206
1090
        os.mkdir(temp_dir)
1217
1101
    
1218
1102
    #apply changes that don't affect filenames
1219
1103
    for entry in changeset.entries.itervalues():
1220
 
        if not entry.is_creation_or_deletion() and not entry.is_boring():
1221
 
            if entry.id not in inventory:
1222
 
                warning("entry {%s} no longer present, can't be updated",
1223
 
                        entry.id)
1224
 
                continue
 
1104
        if not entry.is_creation_or_deletion():
1225
1105
            path = os.path.join(dir, inventory[entry.id])
1226
1106
            entry.apply(path, conflict_handler, reverse)
1227
1107
 
1246
1126
    r_inventory = {}
1247
1127
    for entry in tree.source_inventory().itervalues():
1248
1128
        inventory[entry.id] = entry.path
1249
 
    new_inventory = apply_changeset(cset, r_inventory, tree.basedir,
 
1129
    new_inventory = apply_changeset(cset, r_inventory, tree.root,
1250
1130
                                    reverse=reverse)
1251
1131
    new_entries, remove_entries = \
1252
1132
        get_inventory_change(inventory, new_inventory, cset, reverse)
1387
1267
        return new_meta
1388
1268
    elif new_meta is None:
1389
1269
        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)
 
1270
    elif isinstance(old_meta, ChangeUnixPermissions) and \
 
1271
        isinstance(new_meta, ChangeUnixPermissions):
 
1272
        return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode)
1393
1273
    else:
1394
1274
        return ApplySequence(old_meta, new_meta)
1395
1275
 
1400
1280
            return False
1401
1281
    return True
1402
1282
 
1403
 
class UnsupportedFiletype(Exception):
1404
 
    def __init__(self, kind, full_path):
1405
 
        msg = "The file \"%s\" is a %s, which is not a supported filetype." \
1406
 
            % (full_path, kind)
 
1283
class UnsuppportedFiletype(Exception):
 
1284
    def __init__(self, full_path, stat_result):
 
1285
        msg = "The file \"%s\" is not a supported filetype." % full_path
1407
1286
        Exception.__init__(self, msg)
1408
1287
        self.full_path = full_path
1409
 
        self.kind = kind
 
1288
        self.stat_result = stat_result
1410
1289
 
1411
1290
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1412
1291
    return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1413
1292
 
1414
 
 
1415
1293
class ChangesetGenerator(object):
1416
1294
    def __init__(self, tree_a, tree_b, interesting_ids=None):
1417
1295
        object.__init__(self)
1453
1331
    def get_entry(self, file_id, tree):
1454
1332
        if not tree.has_or_had_id(file_id):
1455
1333
            return None
1456
 
        return tree.inventory[file_id]
 
1334
        return tree.tree.inventory[file_id]
1457
1335
 
1458
1336
    def get_entry_parent(self, entry):
1459
1337
        if entry is None:
1509
1387
 
1510
1388
        if cs_entry is None:
1511
1389
            return None
1512
 
 
1513
 
        cs_entry.metadata_change = self.make_exec_flag_change(id)
1514
 
 
1515
1390
        if id in self.tree_a and id in self.tree_b:
1516
1391
            a_sha1 = self.tree_a.get_file_sha1(id)
1517
1392
            b_sha1 = self.tree_b.get_file_sha1(id)
1518
1393
            if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1519
1394
                return cs_entry
1520
1395
 
1521
 
        cs_entry.contents_change = self.make_contents_change(id)
 
1396
        full_path_a = self.tree_a.readonly_path(id)
 
1397
        full_path_b = self.tree_b.readonly_path(id)
 
1398
        stat_a = self.lstat(full_path_a)
 
1399
        stat_b = self.lstat(full_path_b)
 
1400
        
 
1401
        cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b)
 
1402
        cs_entry.contents_change = self.make_contents_change(full_path_a,
 
1403
                                                             stat_a, 
 
1404
                                                             full_path_b, 
 
1405
                                                             stat_b)
1522
1406
        return cs_entry
1523
1407
 
1524
 
    def make_exec_flag_change(self, file_id):
1525
 
        exec_flag_a = exec_flag_b = None
1526
 
        if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
1527
 
            exec_flag_a = self.tree_a.is_executable(file_id)
1528
 
 
1529
 
        if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
1530
 
            exec_flag_b = self.tree_b.is_executable(file_id)
1531
 
 
1532
 
        if exec_flag_a == exec_flag_b:
1533
 
            return None
1534
 
        return ChangeExecFlag(exec_flag_a, exec_flag_b)
1535
 
 
1536
 
    def make_contents_change(self, file_id):
1537
 
        a_contents = get_contents(self.tree_a, file_id)
1538
 
        b_contents = get_contents(self.tree_b, file_id)
 
1408
    def make_mode_change(self, stat_a, stat_b):
 
1409
        mode_a = None
 
1410
        if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode):
 
1411
            mode_a = stat_a.st_mode & 0777
 
1412
        mode_b = None
 
1413
        if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode):
 
1414
            mode_b = stat_b.st_mode & 0777
 
1415
        if mode_a == mode_b:
 
1416
            return None
 
1417
        return ChangeUnixPermissions(mode_a, mode_b)
 
1418
 
 
1419
    def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b):
 
1420
        if stat_a is None and stat_b is None:
 
1421
            return None
 
1422
        if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\
 
1423
            stat.S_ISDIR(stat_b.st_mode):
 
1424
            return None
 
1425
        if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\
 
1426
            stat.S_ISREG(stat_b.st_mode):
 
1427
            if stat_a.st_ino == stat_b.st_ino and \
 
1428
                stat_a.st_dev == stat_b.st_dev:
 
1429
                return None
 
1430
 
 
1431
        a_contents = self.get_contents(stat_a, full_path_a)
 
1432
        b_contents = self.get_contents(stat_b, full_path_b)
1539
1433
        if a_contents == b_contents:
1540
1434
            return None
1541
1435
        return ReplaceContents(a_contents, b_contents)
1542
1436
 
 
1437
    def get_contents(self, stat_result, full_path):
 
1438
        if stat_result is None:
 
1439
            return None
 
1440
        elif stat.S_ISREG(stat_result.st_mode):
 
1441
            return FileCreate(file(full_path, "rb").read())
 
1442
        elif stat.S_ISDIR(stat_result.st_mode):
 
1443
            return dir_create
 
1444
        elif stat.S_ISLNK(stat_result.st_mode):
 
1445
            return SymlinkCreate(os.readlink(full_path))
 
1446
        else:
 
1447
            raise UnsupportedFiletype(full_path, stat_result)
1543
1448
 
1544
 
def get_contents(tree, file_id):
1545
 
    """Return the appropriate contents to create a copy of file_id from tree"""
1546
 
    if file_id not in tree:
1547
 
        return None
1548
 
    kind = tree.kind(file_id)
1549
 
    if kind == "file":
1550
 
        return TreeFileCreate(tree, file_id)
1551
 
    elif kind in ("directory", "root_directory"):
1552
 
        return dir_create
1553
 
    elif kind == "symlink":
1554
 
        return SymlinkCreate(tree.get_symlink_target(file_id))
1555
 
    else:
1556
 
        raise UnsupportedFiletype(kind, tree.id2path(file_id))
 
1449
    def lstat(self, full_path):
 
1450
        stat_result = None
 
1451
        if full_path is not None:
 
1452
            try:
 
1453
                stat_result = os.lstat(full_path)
 
1454
            except OSError, e:
 
1455
                if e.errno != errno.ENOENT:
 
1456
                    raise
 
1457
        return stat_result
1557
1458
 
1558
1459
 
1559
1460
def full_path(entry, tree):
1560
 
    return os.path.join(tree.basedir, entry.path)
 
1461
    return os.path.join(tree.root, entry.path)
1561
1462
 
1562
1463
def new_delete_entry(entry, tree, inventory, delete):
1563
1464
    if entry.path == "":
1613
1514
            return None
1614
1515
        directory = self.get_dir(id)
1615
1516
        if directory == '.':
1616
 
            directory = u'./.'
 
1517
            directory = './.'
1617
1518
        if directory is None:
1618
1519
            return NULL_ID
1619
1520
        return self.get_rinventory().get(directory)